forked from AkkomaGang/akkoma
Merge branch 'fix/captcha' into 'develop'
Fix account registration when captcha is enabled but not provided Closes #1712 See merge request pleroma/pleroma!2438
This commit is contained in:
commit
2008fa9c7f
11 changed files with 250 additions and 163 deletions
|
@ -202,4 +202,5 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
||||||
- `bio`: optional
|
- `bio`: optional
|
||||||
- `captcha_solution`: optional, contains provider-specific captcha solution,
|
- `captcha_solution`: optional, contains provider-specific captcha solution,
|
||||||
- `captcha_token`: optional, contains provider-specific captcha token
|
- `captcha_token`: optional, contains provider-specific captcha token
|
||||||
|
- `captcha_answer_data`: optional, contains provider-specific captcha data
|
||||||
- `token`: invite token required when the registrations aren't public.
|
- `token`: invite token required when the registrations aren't public.
|
||||||
|
|
|
@ -73,7 +73,6 @@ def start(_type, _args) do
|
||||||
Pleroma.Repo,
|
Pleroma.Repo,
|
||||||
Config.TransferTask,
|
Config.TransferTask,
|
||||||
Pleroma.Emoji,
|
Pleroma.Emoji,
|
||||||
Pleroma.Captcha,
|
|
||||||
Pleroma.Plugs.RateLimiter.Supervisor
|
Pleroma.Plugs.RateLimiter.Supervisor
|
||||||
] ++
|
] ++
|
||||||
cachex_children() ++
|
cachex_children() ++
|
||||||
|
|
|
@ -3,53 +3,22 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha do
|
defmodule Pleroma.Captcha do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
|
|
||||||
alias Calendar.DateTime
|
alias Calendar.DateTime
|
||||||
alias Plug.Crypto.KeyGenerator
|
alias Plug.Crypto.KeyGenerator
|
||||||
alias Plug.Crypto.MessageEncryptor
|
alias Plug.Crypto.MessageEncryptor
|
||||||
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def start_link(_) do
|
|
||||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def init(_) do
|
|
||||||
{:ok, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Ask the configured captcha service for a new captcha
|
Ask the configured captcha service for a new captcha
|
||||||
"""
|
"""
|
||||||
def new do
|
def new do
|
||||||
GenServer.call(__MODULE__, :new)
|
if not enabled?() do
|
||||||
end
|
%{type: :none}
|
||||||
|
|
||||||
@doc """
|
|
||||||
Ask the configured captcha service to validate the captcha
|
|
||||||
"""
|
|
||||||
def validate(token, captcha, answer_data) do
|
|
||||||
GenServer.call(__MODULE__, {:validate, token, captcha, answer_data})
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def handle_call(:new, _from, state) do
|
|
||||||
enabled = Pleroma.Config.get([__MODULE__, :enabled])
|
|
||||||
|
|
||||||
if !enabled do
|
|
||||||
{:reply, %{type: :none}, state}
|
|
||||||
else
|
else
|
||||||
new_captcha = method().new()
|
new_captcha = method().new()
|
||||||
|
|
||||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
|
||||||
|
|
||||||
# This make salt a little different for two keys
|
# This make salt a little different for two keys
|
||||||
token = new_captcha[:token]
|
{secret, sign_secret} = secret_pair(new_captcha[:token])
|
||||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
|
||||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
|
||||||
# Basically copy what Phoenix.Token does here, add the time to
|
# Basically copy what Phoenix.Token does here, add the time to
|
||||||
# the actual data and make it a binary to then encrypt it
|
# the actual data and make it a binary to then encrypt it
|
||||||
encrypted_captcha_answer =
|
encrypted_captcha_answer =
|
||||||
|
@ -60,55 +29,73 @@ def handle_call(:new, _from, state) do
|
||||||
|> :erlang.term_to_binary()
|
|> :erlang.term_to_binary()
|
||||||
|> MessageEncryptor.encrypt(secret, sign_secret)
|
|> MessageEncryptor.encrypt(secret, sign_secret)
|
||||||
|
|
||||||
{
|
|
||||||
:reply,
|
|
||||||
# Replace the answer with the encrypted answer
|
# Replace the answer with the encrypted answer
|
||||||
%{new_captcha | answer_data: encrypted_captcha_answer},
|
%{new_captcha | answer_data: encrypted_captcha_answer}
|
||||||
state
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc """
|
||||||
def handle_call({:validate, token, captcha, answer_data}, _from, state) do
|
Ask the configured captcha service to validate the captcha
|
||||||
|
"""
|
||||||
|
def validate(token, captcha, answer_data) do
|
||||||
|
with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data),
|
||||||
|
:ok <- validate_expiration(at),
|
||||||
|
:ok <- validate_usage(token),
|
||||||
|
:ok <- method().validate(token, captcha, answer_md5),
|
||||||
|
{:ok, _} <- mark_captcha_as_used(token) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false)
|
||||||
|
|
||||||
|
defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid])
|
||||||
|
|
||||||
|
defp secret_pair(token) do
|
||||||
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base])
|
||||||
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
|
||||||
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
|
||||||
|
|
||||||
# If the time found is less than (current_time-seconds_valid) then the time has already passed
|
{secret, sign_secret}
|
||||||
# Later we check that the time found is more than the presumed invalidatation time, that means
|
end
|
||||||
# that the data is still valid and the captcha can be checked
|
|
||||||
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])
|
defp validate_answer_data(token, answer_data) do
|
||||||
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid)
|
{secret, sign_secret} = secret_pair(token)
|
||||||
|
|
||||||
result =
|
|
||||||
with false <- is_nil(answer_data),
|
with false <- is_nil(answer_data),
|
||||||
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
{:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret),
|
||||||
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
%{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do
|
||||||
try do
|
{:ok, %{at: at, answer_data: answer_md5}}
|
||||||
if DateTime.before?(at, valid_if_after),
|
|
||||||
do: throw({:error, dgettext("errors", "CAPTCHA expired")})
|
|
||||||
|
|
||||||
if not is_nil(Cachex.get!(:used_captcha_cache, token)),
|
|
||||||
do: throw({:error, dgettext("errors", "CAPTCHA already used")})
|
|
||||||
|
|
||||||
res = method().validate(token, captcha, answer_md5)
|
|
||||||
# Throw if an error occurs
|
|
||||||
if res != :ok, do: throw(res)
|
|
||||||
|
|
||||||
# Mark this captcha as used
|
|
||||||
{:ok, _} =
|
|
||||||
Cachex.put(:used_captcha_cache, token, true, ttl: :timer.seconds(seconds_valid))
|
|
||||||
|
|
||||||
:ok
|
|
||||||
catch
|
|
||||||
:throw, e -> e
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
_ -> {:error, dgettext("errors", "Invalid answer data")}
|
_ -> {:error, :invalid_answer_data}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{:reply, result, state}
|
defp validate_expiration(created_at) do
|
||||||
|
# If the time found is less than (current_time-seconds_valid) then the time has already passed
|
||||||
|
# Later we check that the time found is more than the presumed invalidatation time, that means
|
||||||
|
# that the data is still valid and the captcha can be checked
|
||||||
|
|
||||||
|
valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid())
|
||||||
|
|
||||||
|
if DateTime.before?(created_at, valid_if_after) do
|
||||||
|
{:error, :expired}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_usage(token) do
|
||||||
|
if is_nil(Cachex.get!(:used_captcha_cache, token)) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :already_used}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mark_captcha_as_used(token) do
|
||||||
|
ttl = seconds_valid() |> :timer.seconds()
|
||||||
|
Cachex.put(:used_captcha_cache, token, true, ttl: ttl)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha.Kocaptcha do
|
defmodule Pleroma.Captcha.Kocaptcha do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
alias Pleroma.Captcha.Service
|
alias Pleroma.Captcha.Service
|
||||||
@behaviour Service
|
@behaviour Service
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ def new do
|
||||||
|
|
||||||
case Tesla.get(endpoint <> "/new") do
|
case Tesla.get(endpoint <> "/new") do
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
%{error: dgettext("errors", "Kocaptcha service unavailable")}
|
%{error: :kocaptcha_service_unavailable}
|
||||||
|
|
||||||
{:ok, res} ->
|
{:ok, res} ->
|
||||||
json_resp = Jason.decode!(res.body)
|
json_resp = Jason.decode!(res.body)
|
||||||
|
@ -33,6 +32,6 @@ def validate(_token, captcha, answer_data) do
|
||||||
if not is_nil(captcha) and
|
if not is_nil(captcha) and
|
||||||
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
|
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
|
||||||
do: :ok,
|
do: :ok,
|
||||||
else: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
else: {:error, :invalid}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Captcha.Native do
|
defmodule Pleroma.Captcha.Native do
|
||||||
import Pleroma.Web.Gettext
|
|
||||||
alias Pleroma.Captcha.Service
|
alias Pleroma.Captcha.Service
|
||||||
@behaviour Service
|
@behaviour Service
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ defmodule Pleroma.Captcha.Native do
|
||||||
def new do
|
def new do
|
||||||
case Captcha.get() do
|
case Captcha.get() do
|
||||||
:error ->
|
:error ->
|
||||||
%{error: dgettext("errors", "Captcha error")}
|
%{error: :captcha_error}
|
||||||
|
|
||||||
{:ok, answer_data, img_binary} ->
|
{:ok, answer_data, img_binary} ->
|
||||||
%{
|
%{
|
||||||
|
@ -25,7 +24,7 @@ def new do
|
||||||
|
|
||||||
@impl Service
|
@impl Service
|
||||||
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
|
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
|
||||||
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
|
def validate(_token, _captcha, _answer), do: {:error, :invalid}
|
||||||
|
|
||||||
defp token do
|
defp token do
|
||||||
10
|
10
|
||||||
|
|
|
@ -94,24 +94,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
|
|
||||||
@doc "POST /api/v1/accounts"
|
@doc "POST /api/v1/accounts"
|
||||||
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||||
params =
|
|
||||||
params
|
|
||||||
|> Map.take([
|
|
||||||
:email,
|
|
||||||
:bio,
|
|
||||||
:captcha_solution,
|
|
||||||
:captcha_token,
|
|
||||||
:captcha_answer_data,
|
|
||||||
:token,
|
|
||||||
:password,
|
|
||||||
:fullname
|
|
||||||
])
|
|
||||||
|> Map.put(:nickname, params.username)
|
|
||||||
|> Map.put(:fullname, Map.get(params, :fullname, params.username))
|
|
||||||
|> Map.put(:confirm, params.password)
|
|
||||||
|> Map.put(:trusted_app, app.trusted)
|
|
||||||
|
|
||||||
with :ok <- validate_email_param(params),
|
with :ok <- validate_email_param(params),
|
||||||
|
:ok <- TwitterAPI.validate_captcha(app, params),
|
||||||
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
|
||||||
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
|
@ -121,7 +105,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
||||||
created_at: Token.Utils.format_created_at(token)
|
created_at: Token.Utils.format_created_at(token)
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
{:error, errors} -> json_response(conn, :bad_request, errors)
|
{:error, error} -> json_response(conn, :bad_request, %{error: error})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,11 +117,11 @@ def create(conn, _) do
|
||||||
render_error(conn, :forbidden, "Invalid credentials")
|
render_error(conn, :forbidden, "Invalid credentials")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_email_param(%{:email => email}) when not is_nil(email), do: :ok
|
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
|
||||||
|
|
||||||
defp validate_email_param(_) do
|
defp validate_email_param(_) do
|
||||||
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
case Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||||
true -> {:error, %{"error" => "Missing parameters"}}
|
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
|
||||||
_ -> :ok
|
_ -> :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,55 +3,28 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
||||||
|
import Pleroma.Web.Gettext
|
||||||
|
|
||||||
alias Pleroma.Emails.Mailer
|
alias Pleroma.Emails.Mailer
|
||||||
alias Pleroma.Emails.UserEmail
|
alias Pleroma.Emails.UserEmail
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.UserInviteToken
|
alias Pleroma.UserInviteToken
|
||||||
|
|
||||||
require Pleroma.Constants
|
|
||||||
|
|
||||||
def register_user(params, opts \\ []) do
|
def register_user(params, opts \\ []) do
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.take([
|
|> Map.take([:email, :token, :password])
|
||||||
:nickname,
|
|> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
|
||||||
:password,
|
|> Map.put(:nickname, params[:username])
|
||||||
:captcha_solution,
|
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|
||||||
:captcha_token,
|
|> Map.put(:password_confirmation, params[:password])
|
||||||
:captcha_answer_data,
|
|
||||||
:token,
|
|
||||||
:email,
|
|
||||||
:trusted_app
|
|
||||||
])
|
|
||||||
|> Map.put(:bio, User.parse_bio(params[:bio] || ""))
|
|
||||||
|> Map.put(:name, params.fullname)
|
|
||||||
|> Map.put(:password_confirmation, params[:confirm])
|
|
||||||
|
|
||||||
case validate_captcha(params) do
|
|
||||||
:ok ->
|
|
||||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||||
create_user(params, opts)
|
create_user(params, opts)
|
||||||
else
|
else
|
||||||
create_user_with_invite(params, opts)
|
create_user_with_invite(params, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
# I have no idea how this error handling works
|
|
||||||
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_captcha(params) do
|
|
||||||
if params[:trusted_app] || not Pleroma.Config.get([Pleroma.Captcha, :enabled]) do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
Pleroma.Captcha.validate(
|
|
||||||
params.captcha_token,
|
|
||||||
params.captcha_solution,
|
|
||||||
params.captcha_answer_data
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_user_with_invite(params, opts) do
|
defp create_user_with_invite(params, opts) do
|
||||||
|
@ -75,16 +48,17 @@ defp create_user(params, opts) do
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
errors =
|
errors =
|
||||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
changeset
|
||||||
|
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
|
|
||||||
{:error, %{error: errors}}
|
{:error, errors}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def password_reset(nickname_or_email) do
|
def password_reset(nickname_or_email) do
|
||||||
with true <- is_binary(nickname_or_email),
|
with true <- is_binary(nickname_or_email),
|
||||||
%User{local: true, email: email} = user when not is_nil(email) <-
|
%User{local: true, email: email} = user when is_binary(email) <-
|
||||||
User.get_by_nickname_or_email(nickname_or_email),
|
User.get_by_nickname_or_email(nickname_or_email),
|
||||||
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
|
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||||
user
|
user
|
||||||
|
@ -106,4 +80,58 @@ def password_reset(nickname_or_email) do
|
||||||
{:error, "unknown user"}
|
{:error, "unknown user"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_captcha(app, params) do
|
||||||
|
if app.trusted || not Pleroma.Captcha.enabled?() do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
do_validate_captcha(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_validate_captcha(params) do
|
||||||
|
with :ok <- validate_captcha_presence(params),
|
||||||
|
:ok <-
|
||||||
|
Pleroma.Captcha.validate(
|
||||||
|
params[:captcha_token],
|
||||||
|
params[:captcha_solution],
|
||||||
|
params[:captcha_answer_data]
|
||||||
|
) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error, :captcha_error} ->
|
||||||
|
captcha_error(dgettext("errors", "CAPTCHA Error"))
|
||||||
|
|
||||||
|
{:error, :invalid} ->
|
||||||
|
captcha_error(dgettext("errors", "Invalid CAPTCHA"))
|
||||||
|
|
||||||
|
{:error, :kocaptcha_service_unavailable} ->
|
||||||
|
captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
|
||||||
|
|
||||||
|
{:error, :expired} ->
|
||||||
|
captcha_error(dgettext("errors", "CAPTCHA expired"))
|
||||||
|
|
||||||
|
{:error, :already_used} ->
|
||||||
|
captcha_error(dgettext("errors", "CAPTCHA already used"))
|
||||||
|
|
||||||
|
{:error, :invalid_answer_data} ->
|
||||||
|
captcha_error(dgettext("errors", "Invalid answer data"))
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
captcha_error(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_captcha_presence(params) do
|
||||||
|
[:captcha_solution, :captcha_token, :captcha_answer_data]
|
||||||
|
|> Enum.find_value(:ok, fn key ->
|
||||||
|
unless is_binary(params[key]) do
|
||||||
|
error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# For some reason FE expects error message to be a serialized JSON
|
||||||
|
defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,7 +61,7 @@ test "new and validate" do
|
||||||
|
|
||||||
assert is_binary(answer)
|
assert is_binary(answer)
|
||||||
assert :ok = Native.validate(token, answer, answer)
|
assert :ok = Native.validate(token, answer, answer)
|
||||||
assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
|
assert {:error, :invalid} == Native.validate(token, answer, answer <> "foobar")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ test "validate" do
|
||||||
|
|
||||||
assert is_binary(answer)
|
assert is_binary(answer)
|
||||||
assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
|
assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer)
|
||||||
|
Cachex.del(:used_captcha_cache, token)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't validate invalid answer" do
|
test "doesn't validate invalid answer" do
|
||||||
|
@ -92,7 +93,7 @@ test "doesn't validate invalid answer" do
|
||||||
|
|
||||||
assert is_binary(answer)
|
assert is_binary(answer)
|
||||||
|
|
||||||
assert {:error, "Invalid answer data"} =
|
assert {:error, :invalid_answer_data} =
|
||||||
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
|
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ test "nil answer_data" do
|
||||||
|
|
||||||
assert is_binary(answer)
|
assert is_binary(answer)
|
||||||
|
|
||||||
assert {:error, "Invalid answer data"} =
|
assert {:error, :invalid_answer_data} =
|
||||||
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
|
Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,12 +6,16 @@ defmodule Pleroma.Captcha.Mock do
|
||||||
alias Pleroma.Captcha.Service
|
alias Pleroma.Captcha.Service
|
||||||
@behaviour Service
|
@behaviour Service
|
||||||
|
|
||||||
|
@solution "63615261b77f5354fb8c4e4986477555"
|
||||||
|
|
||||||
|
def solution, do: @solution
|
||||||
|
|
||||||
@impl Service
|
@impl Service
|
||||||
def new,
|
def new,
|
||||||
do: %{
|
do: %{
|
||||||
type: :mock,
|
type: :mock,
|
||||||
token: "afa1815e14e29355e6c8f6b143a39fa2",
|
token: "afa1815e14e29355e6c8f6b143a39fa2",
|
||||||
answer_data: "63615261b77f5354fb8c4e4986477555",
|
answer_data: @solution,
|
||||||
url: "https://example.org/captcha.png"
|
url: "https://example.org/captcha.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -925,7 +925,8 @@ test "returns bad_request if missing email params when :account_activation_requi
|
||||||
|> Map.put(:remote_ip, {127, 0, 0, 5})
|
|> Map.put(:remote_ip, {127, 0, 0, 5})
|
||||||
|> post("/api/v1/accounts", Map.delete(valid_params, :email))
|
|> post("/api/v1/accounts", Map.delete(valid_params, :email))
|
||||||
|
|
||||||
assert json_response_and_validate_schema(res, 400) == %{"error" => "Missing parameters"}
|
assert json_response_and_validate_schema(res, 400) ==
|
||||||
|
%{"error" => "Missing parameter: email"}
|
||||||
|
|
||||||
res =
|
res =
|
||||||
conn
|
conn
|
||||||
|
@ -1093,6 +1094,91 @@ test "respects rate limit setting", %{conn: conn} do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "create account with enabled captcha" do
|
||||||
|
setup %{conn: conn} do
|
||||||
|
app_token = insert(:oauth_token, user: nil)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer " <> app_token.token)
|
||||||
|
|> put_req_header("content-type", "multipart/form-data")
|
||||||
|
|
||||||
|
[conn: conn]
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do: clear_config([Pleroma.Captcha, :enabled], true)
|
||||||
|
|
||||||
|
test "creates an account and returns 200 if captcha is valid", %{conn: conn} do
|
||||||
|
%{token: token, answer_data: answer_data} = Pleroma.Captcha.new()
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
username: "lain",
|
||||||
|
email: "lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true,
|
||||||
|
captcha_solution: Pleroma.Captcha.Mock.solution(),
|
||||||
|
captcha_token: token,
|
||||||
|
captcha_answer_data: answer_data
|
||||||
|
}
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"access_token" => access_token,
|
||||||
|
"created_at" => _,
|
||||||
|
"scope" => ["read"],
|
||||||
|
"token_type" => "Bearer"
|
||||||
|
} =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/accounts", params)
|
||||||
|
|> json_response_and_validate_schema(:ok)
|
||||||
|
|
||||||
|
assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user)
|
||||||
|
|
||||||
|
Cachex.del(:used_captcha_cache, token)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 400 if any captcha field is not provided", %{conn: conn} do
|
||||||
|
captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data]
|
||||||
|
|
||||||
|
valid_params = %{
|
||||||
|
username: "lain",
|
||||||
|
email: "lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true,
|
||||||
|
captcha_solution: "xx",
|
||||||
|
captcha_token: "xx",
|
||||||
|
captcha_answer_data: "xx"
|
||||||
|
}
|
||||||
|
|
||||||
|
for field <- captcha_fields do
|
||||||
|
expected = %{
|
||||||
|
"error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert expected ==
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/accounts", Map.delete(valid_params, field))
|
||||||
|
|> json_response_and_validate_schema(:bad_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an error if captcha is invalid", %{conn: conn} do
|
||||||
|
params = %{
|
||||||
|
username: "lain",
|
||||||
|
email: "lain@example.org",
|
||||||
|
password: "PlzDontHackLain",
|
||||||
|
agreement: true,
|
||||||
|
captcha_solution: "cofe",
|
||||||
|
captcha_token: "cofe",
|
||||||
|
captcha_answer_data: "cofe"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} ==
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/accounts", params)
|
||||||
|
|> json_response_and_validate_schema(:bad_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "GET /api/v1/accounts/:id/lists - account_lists" do
|
describe "GET /api/v1/accounts/:id/lists - account_lists" do
|
||||||
test "returns lists to which the account belongs" do
|
test "returns lists to which the account belongs" do
|
||||||
%{user: user, conn: conn} = oauth_access(["read:lists"])
|
%{user: user, conn: conn} = oauth_access(["read:lists"])
|
||||||
|
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||||
|
|
||||||
test "it registers a new user and returns the user." do
|
test "it registers a new user and returns the user." do
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "lain",
|
:username => "lain",
|
||||||
:email => "lain@wired.jp",
|
:email => "lain@wired.jp",
|
||||||
:fullname => "lain iwakura",
|
:fullname => "lain iwakura",
|
||||||
:password => "bear",
|
:password => "bear",
|
||||||
|
@ -35,7 +35,7 @@ test "it registers a new user and returns the user." do
|
||||||
|
|
||||||
test "it registers a new user with empty string in bio and returns the user." do
|
test "it registers a new user with empty string in bio and returns the user." do
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "lain",
|
:username => "lain",
|
||||||
:email => "lain@wired.jp",
|
:email => "lain@wired.jp",
|
||||||
:fullname => "lain iwakura",
|
:fullname => "lain iwakura",
|
||||||
:bio => "",
|
:bio => "",
|
||||||
|
@ -60,7 +60,7 @@ test "it sends confirmation email if :account_activation_required is specified i
|
||||||
end
|
end
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "lain",
|
:username => "lain",
|
||||||
:email => "lain@wired.jp",
|
:email => "lain@wired.jp",
|
||||||
:fullname => "lain iwakura",
|
:fullname => "lain iwakura",
|
||||||
:bio => "",
|
:bio => "",
|
||||||
|
@ -87,7 +87,7 @@ test "it sends confirmation email if :account_activation_required is specified i
|
||||||
|
|
||||||
test "it registers a new user and parses mentions in the bio" do
|
test "it registers a new user and parses mentions in the bio" do
|
||||||
data1 = %{
|
data1 = %{
|
||||||
:nickname => "john",
|
:username => "john",
|
||||||
:email => "john@gmail.com",
|
:email => "john@gmail.com",
|
||||||
:fullname => "John Doe",
|
:fullname => "John Doe",
|
||||||
:bio => "test",
|
:bio => "test",
|
||||||
|
@ -98,7 +98,7 @@ test "it registers a new user and parses mentions in the bio" do
|
||||||
{:ok, user1} = TwitterAPI.register_user(data1)
|
{:ok, user1} = TwitterAPI.register_user(data1)
|
||||||
|
|
||||||
data2 = %{
|
data2 = %{
|
||||||
:nickname => "lain",
|
:username => "lain",
|
||||||
:email => "lain@wired.jp",
|
:email => "lain@wired.jp",
|
||||||
:fullname => "lain iwakura",
|
:fullname => "lain iwakura",
|
||||||
:bio => "@john test",
|
:bio => "@john test",
|
||||||
|
@ -123,7 +123,7 @@ test "returns user on success" do
|
||||||
{:ok, invite} = UserInviteToken.create_invite()
|
{:ok, invite} = UserInviteToken.create_invite()
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "vinny",
|
:username => "vinny",
|
||||||
:email => "pasta@pizza.vs",
|
:email => "pasta@pizza.vs",
|
||||||
:fullname => "Vinny Vinesauce",
|
:fullname => "Vinny Vinesauce",
|
||||||
:bio => "streamer",
|
:bio => "streamer",
|
||||||
|
@ -145,7 +145,7 @@ test "returns user on success" do
|
||||||
|
|
||||||
test "returns error on invalid token" do
|
test "returns error on invalid token" do
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -165,7 +165,7 @@ test "returns error on expired token" do
|
||||||
UserInviteToken.update_invite!(invite, used: true)
|
UserInviteToken.update_invite!(invite, used: true)
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -186,7 +186,7 @@ test "returns error on expired token" do
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "vinny",
|
:username => "vinny",
|
||||||
:email => "pasta@pizza.vs",
|
:email => "pasta@pizza.vs",
|
||||||
:fullname => "Vinny Vinesauce",
|
:fullname => "Vinny Vinesauce",
|
||||||
:bio => "streamer",
|
:bio => "streamer",
|
||||||
|
@ -250,7 +250,7 @@ test "returns user on success, after him registration fails" do
|
||||||
UserInviteToken.update_invite!(invite, uses: 99)
|
UserInviteToken.update_invite!(invite, uses: 99)
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "vinny",
|
:username => "vinny",
|
||||||
:email => "pasta@pizza.vs",
|
:email => "pasta@pizza.vs",
|
||||||
:fullname => "Vinny Vinesauce",
|
:fullname => "Vinny Vinesauce",
|
||||||
:bio => "streamer",
|
:bio => "streamer",
|
||||||
|
@ -269,7 +269,7 @@ test "returns user on success, after him registration fails" do
|
||||||
AccountView.render("show.json", %{user: fetched_user})
|
AccountView.render("show.json", %{user: fetched_user})
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -292,7 +292,7 @@ test "returns user on success" do
|
||||||
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
|
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "vinny",
|
:username => "vinny",
|
||||||
:email => "pasta@pizza.vs",
|
:email => "pasta@pizza.vs",
|
||||||
:fullname => "Vinny Vinesauce",
|
:fullname => "Vinny Vinesauce",
|
||||||
:bio => "streamer",
|
:bio => "streamer",
|
||||||
|
@ -317,7 +317,7 @@ test "error after max uses" do
|
||||||
UserInviteToken.update_invite!(invite, uses: 99)
|
UserInviteToken.update_invite!(invite, uses: 99)
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "vinny",
|
:username => "vinny",
|
||||||
:email => "pasta@pizza.vs",
|
:email => "pasta@pizza.vs",
|
||||||
:fullname => "Vinny Vinesauce",
|
:fullname => "Vinny Vinesauce",
|
||||||
:bio => "streamer",
|
:bio => "streamer",
|
||||||
|
@ -335,7 +335,7 @@ test "error after max uses" do
|
||||||
AccountView.render("show.json", %{user: fetched_user})
|
AccountView.render("show.json", %{user: fetched_user})
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -355,7 +355,7 @@ test "returns error on overdue date" do
|
||||||
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
|
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -377,7 +377,7 @@ test "returns error on with overdue date and after max" do
|
||||||
UserInviteToken.update_invite!(invite, uses: 100)
|
UserInviteToken.update_invite!(invite, uses: 100)
|
||||||
|
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "GrimReaper",
|
:username => "GrimReaper",
|
||||||
:email => "death@reapers.afterlife",
|
:email => "death@reapers.afterlife",
|
||||||
:fullname => "Reaper Grim",
|
:fullname => "Reaper Grim",
|
||||||
:bio => "Your time has come",
|
:bio => "Your time has come",
|
||||||
|
@ -395,16 +395,15 @@ test "returns error on with overdue date and after max" do
|
||||||
|
|
||||||
test "it returns the error on registration problems" do
|
test "it returns the error on registration problems" do
|
||||||
data = %{
|
data = %{
|
||||||
:nickname => "lain",
|
:username => "lain",
|
||||||
:email => "lain@wired.jp",
|
:email => "lain@wired.jp",
|
||||||
:fullname => "lain iwakura",
|
:fullname => "lain iwakura",
|
||||||
:bio => "close the world.",
|
:bio => "close the world."
|
||||||
:password => "bear"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{:error, error_object} = TwitterAPI.register_user(data)
|
{:error, error} = TwitterAPI.register_user(data)
|
||||||
|
|
||||||
assert is_binary(error_object[:error])
|
assert is_binary(error)
|
||||||
refute User.get_cached_by_nickname("lain")
|
refute User.get_cached_by_nickname("lain")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue