Merge branch 'issue/209' into 'develop'
[#209] 2FA/two_factor_authentication support See merge request pleroma/pleroma!801
This commit is contained in:
commit
f4c2bf0985
49 changed files with 2184 additions and 34 deletions
|
@ -238,7 +238,18 @@
|
|||
account_field_value_length: 2048,
|
||||
external_user_synchronization: true,
|
||||
extended_nickname_format: true,
|
||||
cleanup_attachments: false
|
||||
cleanup_attachments: false,
|
||||
multi_factor_authentication: [
|
||||
totp: [
|
||||
# digits 6 or 8
|
||||
digits: 6,
|
||||
period: 30
|
||||
],
|
||||
backup_codes: [
|
||||
number: 5,
|
||||
length: 16
|
||||
]
|
||||
]
|
||||
|
||||
config :pleroma, :extensions, output_relationships_in_statuses_by_default: true
|
||||
|
||||
|
|
|
@ -919,6 +919,62 @@
|
|||
key: :external_user_synchronization,
|
||||
type: :boolean,
|
||||
description: "Enabling following/followers counters synchronization for external users"
|
||||
},
|
||||
%{
|
||||
key: :multi_factor_authentication,
|
||||
type: :keyword,
|
||||
description: "Multi-factor authentication settings",
|
||||
suggestions: [
|
||||
[
|
||||
totp: [digits: 6, period: 30],
|
||||
backup_codes: [number: 5, length: 16]
|
||||
]
|
||||
],
|
||||
children: [
|
||||
%{
|
||||
key: :totp,
|
||||
type: :keyword,
|
||||
description: "TOTP settings",
|
||||
suggestions: [digits: 6, period: 30],
|
||||
children: [
|
||||
%{
|
||||
key: :digits,
|
||||
type: :integer,
|
||||
suggestions: [6],
|
||||
description:
|
||||
"Determines the length of a one-time pass-code, in characters. Defaults to 6 characters."
|
||||
},
|
||||
%{
|
||||
key: :period,
|
||||
type: :integer,
|
||||
suggestions: [30],
|
||||
description:
|
||||
"a period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :backup_codes,
|
||||
type: :keyword,
|
||||
description: "MFA backup codes settings",
|
||||
suggestions: [number: 5, length: 16],
|
||||
children: [
|
||||
%{
|
||||
key: :number,
|
||||
type: :integer,
|
||||
suggestions: [5],
|
||||
description: "number of backup codes to generate."
|
||||
},
|
||||
%{
|
||||
key: :length,
|
||||
type: :integer,
|
||||
suggestions: [16],
|
||||
description:
|
||||
"Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -56,6 +56,19 @@
|
|||
ignore_hosts: [],
|
||||
ignore_tld: ["local", "localdomain", "lan"]
|
||||
|
||||
config :pleroma, :instance,
|
||||
multi_factor_authentication: [
|
||||
totp: [
|
||||
# digits 6 or 8
|
||||
digits: 6,
|
||||
period: 30
|
||||
],
|
||||
backup_codes: [
|
||||
number: 2,
|
||||
length: 6
|
||||
]
|
||||
]
|
||||
|
||||
config :web_push_encryption, :vapid_details,
|
||||
subject: "mailto:administrator@example.com",
|
||||
public_key:
|
||||
|
|
|
@ -409,6 +409,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
|
||||
### Get a password reset token for a given nickname
|
||||
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
|
@ -427,6 +428,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
- `nicknames`
|
||||
- Response: none (code `204`)
|
||||
|
||||
## PUT `/api/pleroma/admin/users/disable_mfa`
|
||||
|
||||
### Disable mfa for user's account.
|
||||
|
||||
- Params:
|
||||
- `nickname`
|
||||
- Response: User’s nickname
|
||||
|
||||
## `GET /api/pleroma/admin/users/:nickname/credentials`
|
||||
|
||||
### Get the user's email, password, display and settings-related fields
|
||||
|
|
|
@ -70,7 +70,49 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
|
|||
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
|
||||
* Example response: `{"error": "Invalid password."}`
|
||||
|
||||
## `/api/pleroma/admin/`…
|
||||
## `/api/pleroma/accounts/mfa`
|
||||
#### Gets current MFA settings
|
||||
* method: `GET`
|
||||
* Authentication: required
|
||||
* OAuth scope: `read:security`
|
||||
* Response: JSON. Returns `{"enabled": "false", "totp": false }`
|
||||
|
||||
## `/api/pleroma/accounts/mfa/setup/totp`
|
||||
#### Pre-setup the MFA/TOTP method
|
||||
* method: `GET`
|
||||
* Authentication: required
|
||||
* OAuth scope: `write:security`
|
||||
* Response: JSON. Returns `{"key": [secret_key], "provisioning_uri": "[qr code uri]" }` when successful, otherwise returns HTTP 422 `{"error": "error_msg"}`
|
||||
|
||||
## `/api/pleroma/accounts/mfa/confirm/totp`
|
||||
#### Confirms & enables MFA/TOTP support for user account.
|
||||
* method: `POST`
|
||||
* Authentication: required
|
||||
* OAuth scope: `write:security`
|
||||
* Params:
|
||||
* `password`: user's password
|
||||
* `code`: token from TOTP App
|
||||
* Response: JSON. Returns `{}` if the enable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
|
||||
|
||||
|
||||
## `/api/pleroma/accounts/mfa/totp`
|
||||
#### Disables MFA/TOTP method for user account.
|
||||
* method: `DELETE`
|
||||
* Authentication: required
|
||||
* OAuth scope: `write:security`
|
||||
* Params:
|
||||
* `password`: user's password
|
||||
* Response: JSON. Returns `{}` if the disable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
|
||||
* Example response: `{"error": "Invalid password."}`
|
||||
|
||||
## `/api/pleroma/accounts/mfa/backup_codes`
|
||||
#### Generstes backup codes MFA for user account.
|
||||
* method: `GET`
|
||||
* Authentication: required
|
||||
* OAuth scope: `write:security`
|
||||
* Response: JSON. Returns `{"codes": codes}`when successful, otherwise HTTP 422 `{"error": "[error message]"}`
|
||||
|
||||
## `/api/pleroma/admin/`
|
||||
See [Admin-API](admin_api.md)
|
||||
|
||||
## `/api/v1/pleroma/notifications/read`
|
||||
|
|
|
@ -907,12 +907,18 @@ config :auto_linker,
|
|||
|
||||
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
|
||||
|
||||
|
||||
## :configurable_from_database
|
||||
|
||||
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
|
||||
|
||||
|
||||
### Multi-factor authentication - :two_factor_authentication
|
||||
* `totp` - a list containing TOTP configuration
|
||||
- `digits` - Determines the length of a one-time pass-code in characters. Defaults to 6 characters.
|
||||
- `period` - a period for which the TOTP code will be valid in seconds. Defaults to 30 seconds.
|
||||
* `backup_codes` - a list containing backup codes configuration
|
||||
- `number` - number of backup codes to generate.
|
||||
- `length` - backup code length. Defaults to 16 characters.
|
||||
|
||||
## Restrict entities access for unauthenticated users
|
||||
|
||||
|
@ -930,6 +936,7 @@ Restrict access for unauthenticated users to timelines (public and federate), us
|
|||
* `local`
|
||||
* `remote`
|
||||
|
||||
|
||||
## Pleroma.Web.ApiSpec.CastAndValidate
|
||||
|
||||
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
|
||||
|
|
156
lib/pleroma/mfa.ex
Normal file
156
lib/pleroma/mfa.ex
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA do
|
||||
@moduledoc """
|
||||
The MFA context.
|
||||
"""
|
||||
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.User
|
||||
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
alias Pleroma.MFA.Changeset
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.MFA.TOTP
|
||||
|
||||
@doc """
|
||||
Returns MFA methods the user has enabled.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.MFA.supported_method(User)
|
||||
"totp, u2f"
|
||||
"""
|
||||
@spec supported_methods(User.t()) :: String.t()
|
||||
def supported_methods(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.reduce([], fn m, acc ->
|
||||
if method_enabled?(m, settings) do
|
||||
acc ++ [m]
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|> Enum.join(",")
|
||||
end
|
||||
|
||||
@doc "Checks that user enabled MFA"
|
||||
def require?(user) do
|
||||
fetch_settings(user).enabled
|
||||
end
|
||||
|
||||
@doc """
|
||||
Display MFA settings of user
|
||||
"""
|
||||
def mfa_settings(user) do
|
||||
settings = fetch_settings(user)
|
||||
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|
||||
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def fetch_settings(%User{} = user) do
|
||||
user.multi_factor_authentication_settings || %Settings{}
|
||||
end
|
||||
|
||||
@doc "clears backup codes"
|
||||
def invalidate_backup_code(%User{} = user, hash_code) do
|
||||
%{backup_codes: codes} = fetch_settings(user)
|
||||
|
||||
user
|
||||
|> Changeset.cast_backup_codes(codes -- [hash_code])
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc "generates backup codes"
|
||||
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
||||
def generate_backup_codes(%User{} = user) do
|
||||
with codes <- BackupCodes.generate(),
|
||||
hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
|
||||
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
||||
{:ok, _} <- User.update_and_set_cache(changeset) do
|
||||
{:ok, codes}
|
||||
else
|
||||
{:error, msg} ->
|
||||
%{error: msg}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates secret key and set delivery_type to 'app' for TOTP method.
|
||||
"""
|
||||
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def setup_totp(user) do
|
||||
user
|
||||
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Confirms the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
`code` - TOTP token
|
||||
"""
|
||||
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def confirm_totp(%User{} = user, attrs) do
|
||||
with settings <- user.multi_factor_authentication_settings.totp,
|
||||
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
|
||||
user
|
||||
|> Changeset.confirm_totp()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disables the TOTP method for user.
|
||||
|
||||
`attrs`:
|
||||
`password` - current user password
|
||||
"""
|
||||
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable_totp(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable()
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Force disables all MFA methods for user.
|
||||
"""
|
||||
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def disable(%User{} = user) do
|
||||
user
|
||||
|> Changeset.disable_totp()
|
||||
|> Changeset.disable(true)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has MFA method enabled.
|
||||
"""
|
||||
def method_enabled?(method, settings) do
|
||||
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the user has enabled at least one MFA method.
|
||||
"""
|
||||
def enabled?(settings) do
|
||||
Settings.mfa_methods()
|
||||
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|
||||
|> Enum.any?()
|
||||
end
|
||||
end
|
31
lib/pleroma/mfa/backup_codes.ex
Normal file
31
lib/pleroma/mfa/backup_codes.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.BackupCodes do
|
||||
@moduledoc """
|
||||
This module contains functions for generating backup codes.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :backup_codes]
|
||||
|
||||
@doc """
|
||||
Generates backup codes.
|
||||
"""
|
||||
@spec generate(Keyword.t()) :: list(String.t())
|
||||
def generate(opts \\ []) do
|
||||
number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
|
||||
code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
|
||||
|
||||
Enum.map(1..number_of_codes, fn _ ->
|
||||
:crypto.strong_rand_bytes(div(code_length, 2))
|
||||
|> Base.encode16(case: :lower)
|
||||
end)
|
||||
end
|
||||
|
||||
defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
|
||||
|
||||
defp default_backup_codes_code_length,
|
||||
do: Config.get(@config_ns ++ [:length], 16)
|
||||
end
|
64
lib/pleroma/mfa/changeset.ex
Normal file
64
lib/pleroma/mfa/changeset.ex
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Changeset do
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.User
|
||||
|
||||
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
|
||||
settings =
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> MFA.fetch_settings()
|
||||
|
||||
if force || not MFA.enabled?(settings) do
|
||||
put_change(changeset, %Settings{settings | enabled: false})
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
|
||||
end
|
||||
|
||||
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
|
||||
|
||||
user
|
||||
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
|
||||
end
|
||||
|
||||
def setup_totp(%User{} = user, attrs) do
|
||||
mfa_settings = MFA.fetch_settings(user)
|
||||
|
||||
totp_settings =
|
||||
%Settings.TOTP{}
|
||||
|> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
|
||||
|
||||
user
|
||||
|> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
|
||||
end
|
||||
|
||||
def cast_backup_codes(%User{} = user, codes) do
|
||||
user
|
||||
|> put_change(%Settings{
|
||||
user.multi_factor_authentication_settings
|
||||
| backup_codes: codes
|
||||
})
|
||||
end
|
||||
|
||||
defp put_change(%User{} = user, settings) do
|
||||
user
|
||||
|> Ecto.Changeset.change()
|
||||
|> put_change(settings)
|
||||
end
|
||||
|
||||
defp put_change(%Ecto.Changeset{} = changeset, settings) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
|
||||
end
|
||||
end
|
24
lib/pleroma/mfa/settings.ex
Normal file
24
lib/pleroma/mfa/settings.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Settings do
|
||||
use Ecto.Schema
|
||||
|
||||
@primary_key false
|
||||
|
||||
@mfa_methods [:totp]
|
||||
embedded_schema do
|
||||
field(:enabled, :boolean, default: false)
|
||||
field(:backup_codes, {:array, :string}, default: [])
|
||||
|
||||
embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
|
||||
field(:secret, :string)
|
||||
# app | sms
|
||||
field(:delivery_type, :string, default: "app")
|
||||
field(:confirmed, :boolean, default: false)
|
||||
end
|
||||
end
|
||||
|
||||
def mfa_methods, do: @mfa_methods
|
||||
end
|
106
lib/pleroma/mfa/token.ex
Normal file
106
lib/pleroma/mfa/token.ex
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.Token do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token, as: OAuthToken
|
||||
|
||||
@expires 300
|
||||
|
||||
schema "mfa_tokens" do
|
||||
field(:token, :string)
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:authorization, Authorization)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get_by_token(token) do
|
||||
from(
|
||||
t in __MODULE__,
|
||||
where: t.token == ^token,
|
||||
preload: [:user, :authorization]
|
||||
)
|
||||
|> Repo.find_resource()
|
||||
end
|
||||
|
||||
def validate(token) do
|
||||
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
|
||||
{:expired, false} <- {:expired, is_expired?(token)} do
|
||||
{:ok, token}
|
||||
else
|
||||
{:expired, _} -> {:error, :expired_token}
|
||||
{:fetch_token, _} -> {:error, :not_found}
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def create_token(%User{} = user) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def create_token(user, authorization) do
|
||||
%__MODULE__{}
|
||||
|> change
|
||||
|> assign_user(user)
|
||||
|> assign_authorization(authorization)
|
||||
|> put_token
|
||||
|> put_valid_until
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp assign_user(changeset, user) do
|
||||
changeset
|
||||
|> put_assoc(:user, user)
|
||||
|> validate_required([:user])
|
||||
end
|
||||
|
||||
defp assign_authorization(changeset, authorization) do
|
||||
changeset
|
||||
|> put_assoc(:authorization, authorization)
|
||||
|> validate_required([:authorization])
|
||||
end
|
||||
|
||||
defp put_token(changeset) do
|
||||
changeset
|
||||
|> change(%{token: OAuthToken.Utils.generate_token()})
|
||||
|> validate_required([:token])
|
||||
|> unique_constraint(:token)
|
||||
end
|
||||
|
||||
defp put_valid_until(changeset) do
|
||||
expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
|
||||
|
||||
changeset
|
||||
|> change(%{valid_until: expires_in})
|
||||
|> validate_required([:valid_until])
|
||||
end
|
||||
|
||||
def is_expired?(%__MODULE__{valid_until: valid_until}) do
|
||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
|
||||
end
|
||||
|
||||
def is_expired?(_), do: false
|
||||
|
||||
def delete_expired_tokens do
|
||||
from(
|
||||
q in __MODULE__,
|
||||
where: fragment("?", q.valid_until) < ^Timex.now()
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
end
|
86
lib/pleroma/mfa/totp.ex
Normal file
86
lib/pleroma/mfa/totp.ex
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFA.TOTP do
|
||||
@moduledoc """
|
||||
This module represents functions to create secrets for
|
||||
TOTP Application as well as validate them with a time based token.
|
||||
"""
|
||||
alias Pleroma.Config
|
||||
|
||||
@config_ns [:instance, :multi_factor_authentication, :totp]
|
||||
|
||||
@doc """
|
||||
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
"""
|
||||
def provisioning_uri(secret, label, opts \\ []) do
|
||||
query =
|
||||
%{
|
||||
secret: secret,
|
||||
issuer: Keyword.get(opts, :issuer, default_issuer()),
|
||||
digits: Keyword.get(opts, :digits, default_digits()),
|
||||
period: Keyword.get(opts, :period, default_period())
|
||||
}
|
||||
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|
||||
|> Enum.into(%{})
|
||||
|> URI.encode_query()
|
||||
|
||||
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|
||||
|> URI.to_string()
|
||||
end
|
||||
|
||||
defp default_period, do: Config.get(@config_ns ++ [:period])
|
||||
defp default_digits, do: Config.get(@config_ns ++ [:digits])
|
||||
|
||||
defp default_issuer,
|
||||
do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
|
||||
|
||||
@doc "Creates a random Base 32 encoded string"
|
||||
def generate_secret do
|
||||
Base.encode32(:crypto.strong_rand_bytes(10))
|
||||
end
|
||||
|
||||
@doc "Generates a valid token based on a secret"
|
||||
def generate_token(secret) do
|
||||
:pot.totp(secret)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a given token based on a secret.
|
||||
|
||||
optional parameters:
|
||||
`token_length` default `6`
|
||||
`interval_length` default `30`
|
||||
`window` default 0
|
||||
|
||||
Returns {:ok, :pass} if the token is valid and
|
||||
{:error, :invalid_token} if it is not.
|
||||
"""
|
||||
@spec validate_token(String.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
opts = [
|
||||
token_length: default_digits(),
|
||||
interval_length: default_period()
|
||||
]
|
||||
|
||||
validate_token(secret, token, opts)
|
||||
end
|
||||
|
||||
def validate_token(_, _), do: {:error, :invalid_secret_and_token}
|
||||
|
||||
@doc "See `validate_token/2`"
|
||||
@spec validate_token(String.t(), String.t(), Keyword.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def validate_token(secret, token, options)
|
||||
when is_binary(secret) and is_binary(token) do
|
||||
case :pot.valid_totp(token, secret, options) do
|
||||
true -> {:ok, :pass}
|
||||
false -> {:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
|
||||
end
|
|
@ -15,6 +15,20 @@ def init(options) do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def perform(
|
||||
%{
|
||||
assigns: %{
|
||||
auth_credentials: %{password: _},
|
||||
user: %User{multi_factor_authentication_settings: %{enabled: true}}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
conn
|
||||
|> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
def perform(%{assigns: %{user: %User{}}} = conn, _) do
|
||||
conn
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Formatter
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Registration
|
||||
|
@ -190,6 +191,12 @@ defmodule Pleroma.User do
|
|||
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
|
||||
field(:subscribers, {:array, :string}, default: [])
|
||||
|
||||
embeds_one(
|
||||
:multi_factor_authentication_settings,
|
||||
MFA.Settings,
|
||||
on_replace: :delete
|
||||
)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -927,6 +934,7 @@ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_by_nickname(String.t()) :: User.t() | nil
|
||||
def get_by_nickname(nickname) do
|
||||
Repo.get_by(User, nickname: nickname) ||
|
||||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.ConfigDB
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.ReportNote
|
||||
|
@ -61,6 +62,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
:right_add,
|
||||
:right_add_multiple,
|
||||
:right_delete,
|
||||
:disable_mfa,
|
||||
:right_delete_multiple,
|
||||
:update_user_credentials
|
||||
]
|
||||
|
@ -674,6 +676,18 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
|
|||
json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
@doc "Disable mfa for user's account."
|
||||
def disable_mfa(conn, %{"nickname" => nickname}) do
|
||||
case User.get_by_nickname(nickname) do
|
||||
%User{} = user ->
|
||||
MFA.disable(user)
|
||||
json(conn, nickname)
|
||||
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Show a given user's credentials"
|
||||
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
|
|
|
@ -19,8 +19,8 @@ def get_user(%Plug.Conn{} = conn) do
|
|||
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
|
||||
{:ok, user}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
{:error, _reason} = error -> error
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
45
lib/pleroma/web/auth/totp_authenticator.ex
Normal file
45
lib/pleroma/web/auth/totp_authenticator.ex
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.User
|
||||
|
||||
@doc "Verify code or check backup code."
|
||||
@spec verify(String.t(), User.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||
def verify(
|
||||
token,
|
||||
%User{
|
||||
multi_factor_authentication_settings:
|
||||
%{enabled: true, totp: %{secret: secret, confirmed: true}} = _
|
||||
} = _user
|
||||
)
|
||||
when is_binary(token) and byte_size(token) > 0 do
|
||||
TOTP.validate_token(secret, token)
|
||||
end
|
||||
|
||||
def verify(_, _), do: {:error, :invalid_token}
|
||||
|
||||
@spec verify_recovery_code(User.t(), String.t()) ::
|
||||
{:ok, :pass} | {:error, :invalid_token}
|
||||
def verify_recovery_code(
|
||||
%User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
|
||||
code
|
||||
)
|
||||
when is_list(codes) and is_binary(code) do
|
||||
hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end)
|
||||
|
||||
if hash_code do
|
||||
MFA.invalidate_backup_code(user, hash_code)
|
||||
{:ok, :pass}
|
||||
else
|
||||
{:error, :invalid_token}
|
||||
end
|
||||
end
|
||||
|
||||
def verify_recovery_code(_, _), do: {:error, :invalid_token}
|
||||
end
|
|
@ -402,6 +402,7 @@ defp shortname(name) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def confirm_current_password(user, password) do
|
||||
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
|
||||
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
|
||||
|
|
97
lib/pleroma/web/oauth/mfa_controller.ex
Normal file
97
lib/pleroma/web/oauth/mfa_controller.ex
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.MFAController do
|
||||
@moduledoc """
|
||||
The model represents api to use Multi Factor authentications.
|
||||
"""
|
||||
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||
alias Pleroma.Web.OAuth.MFAView, as: View
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
plug(:fetch_session when action in [:show, :verify])
|
||||
plug(:fetch_flash when action in [:show, :verify])
|
||||
|
||||
@doc """
|
||||
Display form to input mfa code or recovery code.
|
||||
"""
|
||||
def show(conn, %{"mfa_token" => mfa_token} = params) do
|
||||
template = Map.get(params, "challenge_type", "totp")
|
||||
|
||||
conn
|
||||
|> put_view(View)
|
||||
|> render("#{template}.html", %{
|
||||
mfa_token: mfa_token,
|
||||
redirect_uri: params["redirect_uri"],
|
||||
state: params["state"]
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verification code and continue authorization.
|
||||
"""
|
||||
def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do
|
||||
with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||
{:ok, _} <- validates_challenge(user, mfa_params) do
|
||||
conn
|
||||
|> OAuthController.after_create_authorization(auth, %{
|
||||
"authorization" => %{
|
||||
"redirect_uri" => mfa_params["redirect_uri"],
|
||||
"state" => mfa_params["state"]
|
||||
}
|
||||
})
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, "Two-factor authentication failed.")
|
||||
|> put_status(:unauthorized)
|
||||
|> show(mfa_params)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verification second step of MFA (or recovery) and returns access token.
|
||||
|
||||
## Endpoint
|
||||
POST /oauth/mfa/challenge
|
||||
|
||||
params:
|
||||
`client_id`
|
||||
`client_secret`
|
||||
`mfa_token` - access token to check second step of mfa
|
||||
`challenge_type` - 'totp' or 'recovery'
|
||||
`code`
|
||||
|
||||
"""
|
||||
def challenge(conn, %{"mfa_token" => mfa_token} = params) do
|
||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||
{:ok, _} <- validates_challenge(user, params),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, Token.Response.build(user, token))
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(400)
|
||||
|> json(%{error: "Invalid code"})
|
||||
end
|
||||
end
|
||||
|
||||
# Verify TOTP Code
|
||||
defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do
|
||||
TOTPAuthenticator.verify(code, user)
|
||||
end
|
||||
|
||||
# Verify Recovery Code
|
||||
defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do
|
||||
TOTPAuthenticator.verify_recovery_code(user, code)
|
||||
end
|
||||
|
||||
defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type}
|
||||
end
|
8
lib/pleroma/web/oauth/mfa_view.ex
Normal file
8
lib/pleroma/web/oauth/mfa_view.ex
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.MFAView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
end
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
|
@ -14,6 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
alias Pleroma.Web.ControllerHelper
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.MFAController
|
||||
alias Pleroma.Web.OAuth.Scopes
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
|
||||
|
@ -121,7 +123,8 @@ def create_authorization(
|
|||
%{"authorization" => _} = params,
|
||||
opts \\ []
|
||||
) do
|
||||
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
|
||||
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
|
||||
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
|
||||
after_create_authorization(conn, auth, params)
|
||||
else
|
||||
error ->
|
||||
|
@ -179,6 +182,22 @@ defp handle_create_authorization_error(
|
|||
|> authorize(params)
|
||||
end
|
||||
|
||||
defp handle_create_authorization_error(
|
||||
%Plug.Conn{} = conn,
|
||||
{:mfa_required, user, auth, _},
|
||||
params
|
||||
) do
|
||||
{:ok, token} = MFA.Token.create_token(user, auth)
|
||||
|
||||
data = %{
|
||||
"mfa_token" => token.token,
|
||||
"redirect_uri" => params["authorization"]["redirect_uri"],
|
||||
"state" => params["authorization"]["state"]
|
||||
}
|
||||
|
||||
MFAController.show(conn, data)
|
||||
end
|
||||
|
||||
defp handle_create_authorization_error(
|
||||
%Plug.Conn{} = conn,
|
||||
{:account_status, :password_reset_pending},
|
||||
|
@ -231,7 +250,8 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
|
|||
|
||||
json(conn, Token.Response.build(user, token, response_attrs))
|
||||
else
|
||||
_error -> render_invalid_credentials_error(conn)
|
||||
error ->
|
||||
handle_token_exchange_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -244,6 +264,7 @@ def token_exchange(
|
|||
{:account_status, :active} <- {:account_status, User.account_status(user)},
|
||||
{:ok, scopes} <- validate_scopes(app, params),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
|
||||
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, Token.Response.build(user, token))
|
||||
else
|
||||
|
@ -270,13 +291,20 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
|
|||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, Token.Response.build_for_client_credentials(token))
|
||||
else
|
||||
_error -> render_invalid_credentials_error(conn)
|
||||
_error ->
|
||||
handle_token_exchange_error(conn, :invalid_credentails)
|
||||
end
|
||||
end
|
||||
|
||||
# Bad request
|
||||
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
|
||||
|
||||
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(build_and_response_mfa_token(user, auth))
|
||||
end
|
||||
|
||||
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
|
||||
render_error(
|
||||
conn,
|
||||
|
@ -434,7 +462,8 @@ def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs})
|
|||
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
|
||||
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
|
||||
%Registration{} = registration <- Repo.get(Registration, registration_id),
|
||||
{_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
|
||||
{_, {:ok, auth, _user}} <-
|
||||
{:create_authorization, do_create_authorization(conn, params)},
|
||||
%User{} = user <- Repo.preload(auth, :user).user,
|
||||
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
|
||||
conn
|
||||
|
@ -500,8 +529,9 @@ defp do_create_authorization(
|
|||
%App{} = app <- Repo.get_by(App, client_id: client_id),
|
||||
true <- redirect_uri in String.split(app.redirect_uris),
|
||||
{:ok, scopes} <- validate_scopes(app, auth_attrs),
|
||||
{:account_status, :active} <- {:account_status, User.account_status(user)} do
|
||||
Authorization.create_authorization(app, user, scopes)
|
||||
{:account_status, :active} <- {:account_status, User.account_status(user)},
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
|
||||
{:ok, auth, user}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -515,6 +545,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re
|
|||
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
|
||||
do: put_session(conn, :registration_id, registration_id)
|
||||
|
||||
defp build_and_response_mfa_token(user, auth) do
|
||||
with {:ok, token} <- MFA.Token.create_token(user, auth) do
|
||||
Token.Response.build_for_mfa_token(user, token)
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_scopes(App.t(), map()) ::
|
||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
||||
defp validate_scopes(%App{} = app, params) do
|
||||
|
|
38
lib/pleroma/web/oauth/token/clean_worker.ex
Normal file
38
lib/pleroma/web/oauth/token/clean_worker.ex
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.Token.CleanWorker do
|
||||
@moduledoc """
|
||||
The module represents functions to clean an expired OAuth and MFA tokens.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
@ten_seconds 10_000
|
||||
@one_day 86_400_000
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
def start_link(_), do: GenServer.start_link(__MODULE__, %{})
|
||||
|
||||
def init(_) do
|
||||
Process.send_after(self(), :perform, @ten_seconds)
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(:perform, state) do
|
||||
BackgroundWorker.enqueue("clean_expired_tokens", %{})
|
||||
interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
|
||||
|
||||
Process.send_after(self(), :perform, interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def perform(:clean) do
|
||||
OAuth.Token.delete_expired_tokens()
|
||||
MFA.Token.delete_expired_tokens()
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.OAuth.Token.Response do
|
||||
@moduledoc false
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Token.Utils
|
||||
|
||||
|
@ -32,5 +33,13 @@ def build_for_client_credentials(token) do
|
|||
}
|
||||
end
|
||||
|
||||
def build_for_mfa_token(user, mfa_token) do
|
||||
%{
|
||||
error: "mfa_required",
|
||||
mfa_token: mfa_token.token,
|
||||
supported_challenge_types: MFA.supported_methods(user)
|
||||
}
|
||||
end
|
||||
|
||||
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
|
||||
@moduledoc "The module represents actions to manage MFA"
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:security"]} when action in [:setup, :confirm, :disable, :backup_codes]
|
||||
)
|
||||
|
||||
@doc """
|
||||
Gets user multi factor authentication settings
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa
|
||||
|
||||
"""
|
||||
def settings(%{assigns: %{user: user}} = conn, _params) do
|
||||
json(conn, %{settings: MFA.mfa_settings(user)})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepare setup mfa method
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa/setup/[:method]
|
||||
|
||||
"""
|
||||
def setup(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = _params) do
|
||||
with {:ok, user} <- MFA.setup_totp(user),
|
||||
%{secret: secret} = _ <- user.multi_factor_authentication_settings.totp do
|
||||
provisioning_uri = TOTP.provisioning_uri(secret, "#{user.email}")
|
||||
|
||||
json(conn, %{provisioning_uri: provisioning_uri, key: secret})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def setup(conn, _params) do
|
||||
json_response(conn, :bad_request, %{error: "undefined method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Confirms setup and enable mfa method
|
||||
|
||||
## Endpoint
|
||||
POST /api/pleroma/accounts/mfa/confirm/:method
|
||||
|
||||
- params:
|
||||
`code` - confirmation code
|
||||
`password` - current password
|
||||
"""
|
||||
def confirm(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"method" => "totp", "password" => _, "code" => _} = params
|
||||
) do
|
||||
with {:ok, _user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.confirm_totp(user, params) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def confirm(conn, _) do
|
||||
json_response(conn, :bad_request, %{error: "undefined mfa method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disable mfa method and disable mfa if need.
|
||||
"""
|
||||
def disable(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = params) do
|
||||
with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.disable_totp(user) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def disable(%{assigns: %{user: user}} = conn, %{"method" => "mfa"} = params) do
|
||||
with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
|
||||
{:ok, _user} <- MFA.disable(user) do
|
||||
json(conn, %{})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
|
||||
def disable(conn, _) do
|
||||
json_response(conn, :bad_request, %{error: "undefined mfa method"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates backup codes.
|
||||
|
||||
## Endpoint
|
||||
GET /api/pleroma/accounts/mfa/backup_codes
|
||||
|
||||
## Response
|
||||
### Success
|
||||
`{codes: [codes]}`
|
||||
|
||||
### Error
|
||||
`{error: [error_message]}`
|
||||
|
||||
"""
|
||||
def backup_codes(%{assigns: %{user: user}} = conn, _params) do
|
||||
with {:ok, codes} <- MFA.generate_backup_codes(user) do
|
||||
json(conn, %{codes: codes})
|
||||
else
|
||||
{:error, message} ->
|
||||
json_response(conn, :unprocessable_entity, %{error: message})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -132,6 +132,7 @@ defmodule Pleroma.Web.Router do
|
|||
post("/users/follow", AdminAPIController, :user_follow)
|
||||
post("/users/unfollow", AdminAPIController, :user_unfollow)
|
||||
|
||||
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
|
||||
delete("/users", AdminAPIController, :user_delete)
|
||||
post("/users", AdminAPIController, :users_create)
|
||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||
|
@ -258,6 +259,16 @@ defmodule Pleroma.Web.Router do
|
|||
post("/follow_import", UtilController, :follow_import)
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
|
||||
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
|
||||
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
|
||||
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
|
||||
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
|
||||
end
|
||||
|
||||
scope "/oauth", Pleroma.Web.OAuth do
|
||||
scope [] do
|
||||
pipe_through(:oauth)
|
||||
|
@ -269,6 +280,10 @@ defmodule Pleroma.Web.Router do
|
|||
post("/revoke", OAuthController, :token_revoke)
|
||||
get("/registration_details", OAuthController, :registration_details)
|
||||
|
||||
post("/mfa/challenge", MFAController, :challenge)
|
||||
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
|
||||
get("/mfa", MFAController, :show)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:browser)
|
||||
|
||||
|
|
24
lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
Normal file
24
lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex
Normal file
|
@ -0,0 +1,24 @@
|
|||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>Two-factor recovery</h2>
|
||||
|
||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||
<div class="input">
|
||||
<%= label f, :code, "Recovery code" %>
|
||||
<%= text_input f, :code %>
|
||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :challenge_type, value: "recovery" %>
|
||||
</div>
|
||||
|
||||
<%= submit "Verify" %>
|
||||
<% end %>
|
||||
<a href="<%= mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||
Enter a two-factor code
|
||||
</a>
|
24
lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
Normal file
24
lib/pleroma/web/templates/o_auth/mfa/totp.html.eex
Normal file
|
@ -0,0 +1,24 @@
|
|||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>Two-factor authentication</h2>
|
||||
|
||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||
<div class="input">
|
||||
<%= label f, :code, "Authentication code" %>
|
||||
<%= text_input f, :code %>
|
||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :challenge_type, value: "totp" %>
|
||||
</div>
|
||||
|
||||
<%= submit "Verify" %>
|
||||
<% end %>
|
||||
<a href="<%= mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||
Enter a two-factor recovery code
|
||||
</a>
|
|
@ -0,0 +1,13 @@
|
|||
<%= if @error do %>
|
||||
<h2><%= @error %></h2>
|
||||
<% end %>
|
||||
<h2>Two-factor authentication</h2>
|
||||
<p><%= @followee.nickname %></p>
|
||||
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
|
||||
<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
|
||||
<%= text_input f, :code, placeholder: "Authentication code", required: true %>
|
||||
<br>
|
||||
<%= hidden_input f, :id, value: @followee.id %>
|
||||
<%= hidden_input f, :token, value: @mfa_token %>
|
||||
<%= submit "Authorize" %>
|
||||
<% end %>
|
|
@ -8,10 +8,12 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
|
|||
require Logger
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Auth.Authenticator
|
||||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
|
||||
|
@ -68,6 +70,8 @@ defp is_status?(acct) do
|
|||
|
||||
# POST /ostatus_subscribe
|
||||
#
|
||||
# adds a remote account in followers if user already is signed in.
|
||||
#
|
||||
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
|
||||
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
|
||||
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
|
||||
|
@ -78,9 +82,33 @@ def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" =>
|
|||
end
|
||||
end
|
||||
|
||||
# POST /ostatus_subscribe
|
||||
#
|
||||
# step 1.
|
||||
# checks login\password and displays step 2 form of MFA if need.
|
||||
#
|
||||
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
|
||||
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
|
||||
with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
|
||||
{_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
|
||||
{_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
|
||||
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
|
||||
redirect(conn, to: "/users/#{followee.id}")
|
||||
else
|
||||
error ->
|
||||
handle_follow_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
# POST /ostatus_subscribe
|
||||
#
|
||||
# step 2
|
||||
# checks TOTP code. otherwise displays form with errors
|
||||
#
|
||||
def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
|
||||
with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
|
||||
{_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
|
||||
{_, _, _, {:ok, _}} <-
|
||||
{:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
|
||||
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
|
||||
redirect(conn, to: "/users/#{followee.id}")
|
||||
else
|
||||
|
@ -94,6 +122,23 @@ def do_follow(%{assigns: %{user: nil}} = conn, _) do
|
|||
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
|
||||
end
|
||||
|
||||
defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
|
||||
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
|
||||
end
|
||||
|
||||
defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
|
||||
render(conn, "follow_mfa.html", %{
|
||||
error: "Wrong authentication code",
|
||||
followee: followee,
|
||||
mfa_token: token
|
||||
})
|
||||
end
|
||||
|
||||
defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
|
||||
{:ok, %{token: token}} = MFA.Token.create_token(user)
|
||||
render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
|
||||
end
|
||||
|
||||
defp handle_follow_error(conn, {:auth, _, followee} = _) do
|
||||
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
|
||||
end
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -176,6 +176,7 @@ defp deps do
|
|||
{:quack, "~> 0.1.1"},
|
||||
{:joken, "~> 2.0"},
|
||||
{:benchee, "~> 1.0"},
|
||||
{:pot, "~> 0.10.2"},
|
||||
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
|
||||
{:ex_const, "~> 0.2"},
|
||||
{:plug_static_index_html, "~> 1.0.0"},
|
||||
|
|
33
mix.lock
33
mix.lock
|
@ -2,8 +2,7 @@
|
|||
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
|
||||
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]},
|
||||
"base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "3b29948de2013d3f93aa898c884a9dff847e7aec75d9d6d8c1dc4c61c2716c42"},
|
||||
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
||||
"bbcode": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/bbcode.git", "f2d267675e9a7e1ad1ea9beb4cc23382762b66c2", [ref: "v0.2.0"]},
|
||||
"bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"},
|
||||
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
|
||||
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
|
@ -19,38 +18,33 @@
|
|||
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
|
||||
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
|
||||
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0bbd3222607ccaaac5c0340f7f525c627ae4d7aee6c8c8c108922620c5b6446"},
|
||||
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
|
||||
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
|
||||
"db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
|
||||
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
|
||||
"ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5eed18252f5b5bbadec56a24112b531343507dbe046273133176b12190ce19cc"},
|
||||
"ecto": {:hex, :ecto, "3.4.2", "6890af71025769bd27ef62b1ed1925cfe23f7f0460bcb3041da4b705215ff23e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3959b8a83e086202a4bd86b4b5e6e71f9f1840813de14a57d502d3fc2ef7132"},
|
||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
|
||||
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
|
||||
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
|
||||
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
|
||||
"ex_aws": {:hex, :ex_aws, "2.1.1", "1e4de2106cfbf4e837de41be41cd15813eabc722315e388f0d6bb3732cec47cd", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "06b6fde12b33bb6d65d5d3493e903ba5a56d57a72350c15285a4298338089e10"},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0569f5b211b1a3b12b705fe2a9d0e237eb1360b9d76298028df2346cad13097a"},
|
||||
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"},
|
||||
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"},
|
||||
"fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
|
||||
"fast_html": {:hex, :fast_html, "1.0.1", "5bc7df4dc4607ec2c314c16414e4111d79a209956c4f5df96602d194c61197f9", [:make, :mix], [], "hexpm", "18e627dd62051a375ef94b197f41e8027c3e8eef0180ab8f81e0543b3dc6900a"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.1.6", "60a5ae96879956dea409a91a77f5dd2994c24cc10f80eefd8f9892ee4c0c7b25", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b73f50f0cb522dd0331ea8e8c90b408de42c50f37641219d6364f0e3e7efd22c"},
|
||||
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
|
||||
"floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "631f4e627c46d5ecd347df5a2accdaf0621c77c3693c5b75a8ad58e84c61f242"},
|
||||
"floki": {:hex, :floki, "0.26.0", "4df88977e2e357c6720e1b650f613444bfb48c5acfc6a0c646ab007d08ad13bf", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e7b66ce7feef5518a9cd9fc7b52dd62a64028bd9cb6d6ad282a0f0fc90a4ae52"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
|
||||
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
|
||||
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
|
||||
"gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]},
|
||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
|
||||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
|
||||
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
|
||||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
|
||||
|
@ -59,37 +53,34 @@
|
|||
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
|
||||
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
|
||||
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
|
||||
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
|
||||
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
|
||||
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
|
||||
"mock": {:hex, :mock, "0.3.4", "c5862eb3b8c64237f45f586cf00c9d892ba07bb48305a43319d428ce3c2897dd", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "e6d886252f1a41f4ba06ecf2b4c8d38760b34b1c08a11c28f7397b2e03995964"},
|
||||
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm", "3bc928d817974fa10cc11e6c89b9a9361e37e96dbbf3d868c41094ec05745dcd"},
|
||||
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"},
|
||||
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
|
||||
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
|
||||
"oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
|
||||
"open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "b862ebd78de0df95875cf46feb6e9607130dc2a8", [ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"]},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.12", "b86fa85a2ba336f5de068549de5ccceec356fd413264a9637e7733395d6cc4ea", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "58331ade6d77e1312a3d976f0fa41803b8f004b2b5f489193425bc46aea3ed30"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b0bb30eda478a06dbfbe96728061a93833db3861a49ccb516f839ecb08493fbb"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "ebf1bfa7b3c1c850c04929afe02e2e0d7ab135e0706332c865de03e761676b1f"},
|
||||
"plug": {:hex, :plug, "1.9.0", "8d7c4e26962283ff9f8f3347bd73838e2413fbc38b7bb5467d5924f68f3a5a4a", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9902eda2c52ada2a096434682e99a2493f5d06a94d6ac6bcfff9805f952350f1"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7d722581ce865a237e14da6d946f92704101740a256bd13ec91e63c0b122fc70"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
|
||||
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.1.1", "a196e4f428d7f5d6dba5ded314cc55cd0fbddf1110af620f75c0190e77844b33", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "15a3c34ffaccef8a0b575b8d39ab1b9044586d7dab917292cdc44cf2737df7f2"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"},
|
||||
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
|
||||
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
|
||||
"prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"},
|
||||
"pot": {:hex, :pot, "0.10.2", "9895c83bcff8cd22d9f5bc79dfc88a188176b261b618ad70d93faf5c5ca36e67", [:rebar3], [], "hexpm", "ac589a8e296b7802681e93cd0a436faec117ea63e9916709c628df31e17e91e2"},
|
||||
"prometheus": {:hex, :prometheus, "4.4.1", "1e96073b3ed7788053768fea779cbc896ddc3bdd9ba60687f2ad50b252ac87d6", [:mix, :rebar3], [], "hexpm", "d39f2ce1f3f29f3bf04f915aa3cf9c7cd4d2cee2f975e05f526e06cae9b7c902"},
|
||||
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
|
||||
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"},
|
||||
"prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"},
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddMultiFactorAuthenticationSettingsToUser do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add(:multi_factor_authentication_settings, :map, default: %{})
|
||||
end
|
||||
end
|
||||
end
|
16
priv/repo/migrations/20190508193213_create_mfa_tokens.exs
Normal file
16
priv/repo/migrations/20190508193213_create_mfa_tokens.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateMfaTokens do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:mfa_tokens) do
|
||||
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
add(:authorization_id, references(:oauth_authorizations, on_delete: :delete_all))
|
||||
add(:token, :string)
|
||||
add(:valid_until, :naive_datetime_usec)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create(unique_index(:mfa_tokens, :token))
|
||||
end
|
||||
end
|
Binary file not shown.
Binary file not shown.
11
test/mfa/backup_codes_test.exs
Normal file
11
test/mfa/backup_codes_test.exs
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.MFA.BackupCodesTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
|
||||
test "generate backup codes" do
|
||||
codes = BackupCodes.generate(number_of_codes: 2, length: 4)
|
||||
|
||||
assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes
|
||||
end
|
||||
end
|
17
test/mfa/totp_test.exs
Normal file
17
test/mfa/totp_test.exs
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Pleroma.MFA.TOTPTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.MFA.TOTP
|
||||
|
||||
test "create provisioning_uri to generate qrcode" do
|
||||
uri =
|
||||
TOTP.provisioning_uri("test-secrcet", "test@example.com",
|
||||
issuer: "Plerome-42",
|
||||
digits: 8,
|
||||
period: 60
|
||||
)
|
||||
|
||||
assert uri ==
|
||||
"otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet"
|
||||
end
|
||||
end
|
53
test/mfa_test.exs
Normal file
53
test/mfa_test.exs
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MFATest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Pleroma.Factory
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.MFA
|
||||
|
||||
describe "mfa_settings" do
|
||||
test "returns settings user's" do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
settings = MFA.mfa_settings(user)
|
||||
assert match?(^settings, %{enabled: true, totp: true})
|
||||
end
|
||||
end
|
||||
|
||||
describe "generate backup codes" do
|
||||
test "returns backup codes" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, [code1, code2]} = MFA.generate_backup_codes(user)
|
||||
updated_user = refresh_record(user)
|
||||
[hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes
|
||||
assert Pbkdf2.checkpw(code1, hash1)
|
||||
assert Pbkdf2.checkpw(code2, hash2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "invalidate_backup_code" do
|
||||
test "invalid used code" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, _} = MFA.generate_backup_codes(user)
|
||||
user = refresh_record(user)
|
||||
assert length(user.multi_factor_authentication_settings.backup_codes) == 2
|
||||
[hash_code | _] = user.multi_factor_authentication_settings.backup_codes
|
||||
|
||||
{:ok, user} = MFA.invalidate_backup_code(user, hash_code)
|
||||
|
||||
assert length(user.multi_factor_authentication_settings.backup_codes) == 1
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,6 +24,31 @@ test "it continues if a user is assigned", %{conn: conn} do
|
|||
end
|
||||
end
|
||||
|
||||
test "it halts if user is assigned and MFA enabled", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}})
|
||||
|> assign(:auth_credentials, %{password: "xd-42"})
|
||||
|> EnsureAuthenticatedPlug.call(%{})
|
||||
|
||||
assert conn.status == 403
|
||||
assert conn.halted == true
|
||||
|
||||
assert conn.resp_body ==
|
||||
"{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}"
|
||||
end
|
||||
|
||||
test "it continues if user is assigned and MFA disabled", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}})
|
||||
|> assign(:auth_credentials, %{password: "xd-42"})
|
||||
|> EnsureAuthenticatedPlug.call(%{})
|
||||
|
||||
refute conn.status == 403
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
describe "with :if_func / :unless_func options" do
|
||||
setup do
|
||||
%{
|
||||
|
|
|
@ -11,6 +11,7 @@ def build(data \\ %{}) do
|
|||
bio: "A tester.",
|
||||
ap_id: "some id",
|
||||
last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
|
||||
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
|
||||
notification_settings: %Pleroma.User.NotificationSetting{}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,8 @@ def user_factory do
|
|||
bio: sequence(:bio, &"Tester Number #{&1}"),
|
||||
last_digest_emailed_at: NaiveDateTime.utc_now(),
|
||||
last_refreshed_at: NaiveDateTime.utc_now(),
|
||||
notification_settings: %Pleroma.User.NotificationSetting{}
|
||||
notification_settings: %Pleroma.User.NotificationSetting{},
|
||||
multi_factor_authentication_settings: %Pleroma.MFA.Settings{}
|
||||
}
|
||||
|
||||
%{
|
||||
|
@ -422,4 +423,13 @@ def marker_factory do
|
|||
last_read_id: "1"
|
||||
}
|
||||
end
|
||||
|
||||
def mfa_token_factory do
|
||||
%Pleroma.MFA.Token{
|
||||
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
|
||||
authorization: build(:oauth_authorization),
|
||||
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
|
||||
user: build(:user)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -172,6 +172,7 @@ test "works with URIs" do
|
|||
|> Map.put(:search_rank, nil)
|
||||
|> Map.put(:search_type, nil)
|
||||
|> Map.put(:last_digest_emailed_at, nil)
|
||||
|> Map.put(:multi_factor_authentication_settings, nil)
|
||||
|> Map.put(:notification_settings, nil)
|
||||
|
||||
assert user == expected
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.ConfigDB
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ReportNote
|
||||
|
@ -1278,6 +1279,38 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi
|
|||
"@#{admin.nickname} deactivated users: @#{user.nickname}"
|
||||
end
|
||||
|
||||
describe "PUT disable_mfa" do
|
||||
test "returns 200 and disable 2fa", %{conn: conn} do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname})
|
||||
|> json_response(200)
|
||||
|
||||
assert response == user.nickname
|
||||
mfa_settings = refresh_record(user).multi_factor_authentication_settings
|
||||
|
||||
refute mfa_settings.enabled
|
||||
refute mfa_settings.totp.confirmed
|
||||
end
|
||||
|
||||
test "returns 404 if user not found", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"})
|
||||
|> json_response(404)
|
||||
|
||||
assert response == "Not found"
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/pleroma/admin/users/invite_token" do
|
||||
test "without options", %{conn: conn} do
|
||||
conn = post(conn, "/api/pleroma/admin/users/invite_token")
|
||||
|
|
43
test/web/auth/pleroma_authenticator_test.exs
Normal file
43
test/web/auth/pleroma_authenticator_test.exs
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Web.Auth.PleromaAuthenticator
|
||||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
password = "testpassword"
|
||||
name = "AgentSmith"
|
||||
user = insert(:user, nickname: name, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
|
||||
{:ok, [user: user, name: name, password: password]}
|
||||
end
|
||||
|
||||
test "get_user/authorization", %{user: user, name: name, password: password} do
|
||||
params = %{"authorization" => %{"name" => name, "password" => password}}
|
||||
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
|
||||
|
||||
assert {:ok, user} == res
|
||||
end
|
||||
|
||||
test "get_user/authorization with invalid password", %{name: name} do
|
||||
params = %{"authorization" => %{"name" => name, "password" => "password"}}
|
||||
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
|
||||
|
||||
assert {:error, {:checkpw, false}} == res
|
||||
end
|
||||
|
||||
test "get_user/grant_type_password", %{user: user, name: name, password: password} do
|
||||
params = %{"grant_type" => "password", "username" => name, "password" => password}
|
||||
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
|
||||
|
||||
assert {:ok, user} == res
|
||||
end
|
||||
|
||||
test "error credintails" do
|
||||
res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}})
|
||||
assert {:error, :invalid_credentials} == res
|
||||
end
|
||||
end
|
51
test/web/auth/totp_authenticator_test.exs
Normal file
51
test/web/auth/totp_authenticator_test.exs
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "verify token" do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
otp_token = TOTP.generate_token(otp_secret)
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass}
|
||||
assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token}
|
||||
assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token}
|
||||
end
|
||||
|
||||
test "checks backup codes" do
|
||||
[code | _] = backup_codes = BackupCodes.generate()
|
||||
|
||||
hashed_codes =
|
||||
backup_codes
|
||||
|> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1))
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
backup_codes: hashed_codes,
|
||||
totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass}
|
||||
refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass}
|
||||
end
|
||||
end
|
306
test/web/oauth/mfa_controller_test.exs
Normal file
306
test/web/oauth/mfa_controller_test.exs
Normal file
|
@ -0,0 +1,306 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.MFAControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.BackupCodes
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
|
||||
setup %{conn: conn} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
backup_codes: [Comeonin.Pbkdf2.hashpwsalt("test-code")],
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
app = insert(:oauth_app)
|
||||
{:ok, conn: conn, user: user, app: app}
|
||||
end
|
||||
|
||||
describe "show" do
|
||||
setup %{conn: conn, user: user, app: app} do
|
||||
mfa_token =
|
||||
insert(:mfa_token,
|
||||
user: user,
|
||||
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
|
||||
)
|
||||
|
||||
{:ok, conn: conn, mfa_token: mfa_token}
|
||||
end
|
||||
|
||||
test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/oauth/mfa",
|
||||
%{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"state" => "a_state",
|
||||
"redirect_uri" => "http://localhost:8080/callback"
|
||||
}
|
||||
)
|
||||
|
||||
assert response = html_response(conn, 200)
|
||||
assert response =~ "Two-factor authentication"
|
||||
assert response =~ mfa_token.token
|
||||
assert response =~ "http://localhost:8080/callback"
|
||||
end
|
||||
|
||||
test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/oauth/mfa",
|
||||
%{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"state" => "a_state",
|
||||
"redirect_uri" => "http://localhost:8080/callback",
|
||||
"challenge_type" => "recovery"
|
||||
}
|
||||
)
|
||||
|
||||
assert response = html_response(conn, 200)
|
||||
assert response =~ "Two-factor recovery"
|
||||
assert response =~ mfa_token.token
|
||||
assert response =~ "http://localhost:8080/callback"
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify" do
|
||||
setup %{conn: conn, user: user, app: app} do
|
||||
mfa_token =
|
||||
insert(:mfa_token,
|
||||
user: user,
|
||||
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
|
||||
)
|
||||
|
||||
{:ok, conn: conn, user: user, mfa_token: mfa_token, app: app}
|
||||
end
|
||||
|
||||
test "POST /oauth/mfa/verify, verify totp code", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
mfa_token: mfa_token,
|
||||
app: app
|
||||
} do
|
||||
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> post("/oauth/mfa/verify", %{
|
||||
"mfa" => %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "totp",
|
||||
"code" => otp_token,
|
||||
"state" => "a_state",
|
||||
"redirect_uri" => OAuthController.default_redirect_uri(app)
|
||||
}
|
||||
})
|
||||
|
||||
target = redirected_to(conn)
|
||||
target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
|
||||
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
|
||||
assert %{"state" => "a_state", "code" => code} = query
|
||||
assert target_url == OAuthController.default_redirect_uri(app)
|
||||
auth = Repo.get_by(Authorization, token: code)
|
||||
assert auth.scopes == ["write"]
|
||||
end
|
||||
|
||||
test "POST /oauth/mfa/verify, verify recovery code", %{
|
||||
conn: conn,
|
||||
mfa_token: mfa_token,
|
||||
app: app
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> post("/oauth/mfa/verify", %{
|
||||
"mfa" => %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "recovery",
|
||||
"code" => "test-code",
|
||||
"state" => "a_state",
|
||||
"redirect_uri" => OAuthController.default_redirect_uri(app)
|
||||
}
|
||||
})
|
||||
|
||||
target = redirected_to(conn)
|
||||
target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
|
||||
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
|
||||
assert %{"state" => "a_state", "code" => code} = query
|
||||
assert target_url == OAuthController.default_redirect_uri(app)
|
||||
auth = Repo.get_by(Authorization, token: code)
|
||||
assert auth.scopes == ["write"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "challenge/totp" do
|
||||
test "returns access token with valid code", %{conn: conn, user: user, app: app} do
|
||||
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
|
||||
|
||||
mfa_token =
|
||||
insert(:mfa_token,
|
||||
user: user,
|
||||
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
|
||||
)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "totp",
|
||||
"code" => otp_token,
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(:ok)
|
||||
|
||||
ap_id = user.ap_id
|
||||
|
||||
assert match?(
|
||||
%{
|
||||
"access_token" => _,
|
||||
"expires_in" => 600,
|
||||
"me" => ^ap_id,
|
||||
"refresh_token" => _,
|
||||
"scope" => "write",
|
||||
"token_type" => "Bearer"
|
||||
},
|
||||
response
|
||||
)
|
||||
end
|
||||
|
||||
test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
|
||||
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => "XXX",
|
||||
"challenge_type" => "totp",
|
||||
"code" => otp_token,
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(400)
|
||||
|
||||
assert response == %{"error" => "Invalid code"}
|
||||
end
|
||||
|
||||
test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
|
||||
mfa_token = insert(:mfa_token, user: user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "totp",
|
||||
"code" => "XXX",
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(400)
|
||||
|
||||
assert response == %{"error" => "Invalid code"}
|
||||
end
|
||||
|
||||
test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
|
||||
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
|
||||
mfa_token = insert(:mfa_token, user: user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "totp",
|
||||
"code" => otp_token,
|
||||
"client_id" => "xxx",
|
||||
"client_secret" => "xxx"
|
||||
})
|
||||
|> json_response(400)
|
||||
|
||||
assert response == %{"error" => "Invalid code"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "challenge/recovery" do
|
||||
setup %{conn: conn} do
|
||||
app = insert(:oauth_app)
|
||||
{:ok, conn: conn, app: app}
|
||||
end
|
||||
|
||||
test "returns access token with valid code", %{conn: conn, app: app} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
[code | _] = backup_codes = BackupCodes.generate()
|
||||
|
||||
hashed_codes =
|
||||
backup_codes
|
||||
|> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1))
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
backup_codes: hashed_codes,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
mfa_token =
|
||||
insert(:mfa_token,
|
||||
user: user,
|
||||
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
|
||||
)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "recovery",
|
||||
"code" => code,
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(:ok)
|
||||
|
||||
ap_id = user.ap_id
|
||||
|
||||
assert match?(
|
||||
%{
|
||||
"access_token" => _,
|
||||
"expires_in" => 600,
|
||||
"me" => ^ap_id,
|
||||
"refresh_token" => _,
|
||||
"scope" => "write",
|
||||
"token_type" => "Bearer"
|
||||
},
|
||||
response
|
||||
)
|
||||
|
||||
error_response =
|
||||
conn
|
||||
|> post("/oauth/mfa/challenge", %{
|
||||
"mfa_token" => mfa_token.token,
|
||||
"challenge_type" => "recovery",
|
||||
"code" => code,
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(400)
|
||||
|
||||
assert error_response == %{"error" => "Invalid code"}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
|
|||
use Pleroma.Web.ConnCase
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
|
@ -604,6 +606,41 @@ test "redirects with oauth authorization, " <>
|
|||
end
|
||||
end
|
||||
|
||||
test "redirect to on two-factor auth page" do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
app = insert(:oauth_app, scopes: ["read", "write", "follow"])
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> post("/oauth/authorize", %{
|
||||
"authorization" => %{
|
||||
"name" => user.nickname,
|
||||
"password" => "test",
|
||||
"client_id" => app.client_id,
|
||||
"redirect_uri" => app.redirect_uris,
|
||||
"scope" => "read write",
|
||||
"state" => "statepassed"
|
||||
}
|
||||
})
|
||||
|
||||
result = html_response(conn, 200)
|
||||
|
||||
mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
|
||||
assert result =~ app.redirect_uris
|
||||
assert result =~ "statepassed"
|
||||
assert result =~ mfa_token.token
|
||||
assert result =~ "Two-factor authentication"
|
||||
end
|
||||
|
||||
test "returns 401 for wrong credentials", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
app = insert(:oauth_app)
|
||||
|
@ -735,6 +772,46 @@ test "issues a token for `password` grant_type with valid credentials, with full
|
|||
assert token.scopes == app.scopes
|
||||
end
|
||||
|
||||
test "issues a mfa token for `password` grant_type, when MFA enabled" do
|
||||
password = "testpassword"
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
app = insert(:oauth_app, scopes: ["read", "write"])
|
||||
|
||||
response =
|
||||
build_conn()
|
||||
|> post("/oauth/token", %{
|
||||
"grant_type" => "password",
|
||||
"username" => user.nickname,
|
||||
"password" => password,
|
||||
"client_id" => app.client_id,
|
||||
"client_secret" => app.client_secret
|
||||
})
|
||||
|> json_response(403)
|
||||
|
||||
assert match?(
|
||||
%{
|
||||
"supported_challenge_types" => "totp",
|
||||
"mfa_token" => _,
|
||||
"error" => "mfa_required"
|
||||
},
|
||||
response
|
||||
)
|
||||
|
||||
token = Repo.get_by(MFA.Token, token: response["mfa_token"])
|
||||
assert token.user_id == user.id
|
||||
assert token.authorization_id
|
||||
end
|
||||
|
||||
test "issues a token for request with HTTP basic auth client credentials" do
|
||||
user = insert(:user)
|
||||
app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
import Pleroma.Factory
|
||||
alias Pleroma.MFA.Settings
|
||||
alias Pleroma.MFA.TOTP
|
||||
|
||||
describe "GET /api/pleroma/accounts/mfa/settings" do
|
||||
test "returns user mfa settings for new user", %{conn: conn} do
|
||||
token = insert(:oauth_token, scopes: ["read", "follow"])
|
||||
token2 = insert(:oauth_token, scopes: ["write"])
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/pleroma/accounts/mfa")
|
||||
|> json_response(:ok) == %{
|
||||
"settings" => %{"enabled" => false, "totp" => false}
|
||||
}
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> get("/api/pleroma/accounts/mfa")
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: read:security."
|
||||
}
|
||||
end
|
||||
|
||||
test "returns user mfa settings with enabled totp", %{conn: conn} do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
enabled: true,
|
||||
totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["read", "follow"], user: user)
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/pleroma/accounts/mfa")
|
||||
|> json_response(:ok) == %{
|
||||
"settings" => %{"enabled" => true, "totp" => true}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/pleroma/accounts/mfa/backup_codes" do
|
||||
test "returns backup codes", %{conn: conn} do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
backup_codes: ["1", "2", "3"],
|
||||
totp: %Settings.TOTP{secret: "secret"}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
token2 = insert(:oauth_token, scopes: ["read"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/pleroma/accounts/mfa/backup_codes")
|
||||
|> json_response(:ok)
|
||||
|
||||
assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"]
|
||||
user = refresh_record(user)
|
||||
mfa_settings = user.multi_factor_authentication_settings
|
||||
assert mfa_settings.totp.secret == "secret"
|
||||
refute mfa_settings.backup_codes == ["1", "2", "3"]
|
||||
refute mfa_settings.backup_codes == []
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> get("/api/pleroma/accounts/mfa/backup_codes")
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: write:security."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/pleroma/accounts/mfa/setup/totp" do
|
||||
test "return errors when method is invalid", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/pleroma/accounts/mfa/setup/torf")
|
||||
|> json_response(400)
|
||||
|
||||
assert response == %{"error" => "undefined method"}
|
||||
end
|
||||
|
||||
test "returns key and provisioning_uri", %{conn: conn} do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
token2 = insert(:oauth_token, scopes: ["read"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> get("/api/pleroma/accounts/mfa/setup/totp")
|
||||
|> json_response(:ok)
|
||||
|
||||
user = refresh_record(user)
|
||||
mfa_settings = user.multi_factor_authentication_settings
|
||||
secret = mfa_settings.totp.secret
|
||||
refute mfa_settings.enabled
|
||||
assert mfa_settings.backup_codes == ["1", "2", "3"]
|
||||
|
||||
assert response == %{
|
||||
"key" => secret,
|
||||
"provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}")
|
||||
}
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> get("/api/pleroma/accounts/mfa/setup/totp")
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: write:security."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/pleroma/accounts/mfa/confirm/totp" do
|
||||
test "returns success result", %{conn: conn} do
|
||||
secret = TOTP.generate_secret()
|
||||
code = TOTP.generate_token(secret)
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
backup_codes: ["1", "2", "3"],
|
||||
totp: %Settings.TOTP{secret: secret}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
token2 = insert(:oauth_token, scopes: ["read"])
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
|
||||
|> json_response(:ok)
|
||||
|
||||
settings = refresh_record(user).multi_factor_authentication_settings
|
||||
assert settings.enabled
|
||||
assert settings.totp.secret == secret
|
||||
assert settings.totp.confirmed
|
||||
assert settings.backup_codes == ["1", "2", "3"]
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code})
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: write:security."
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error if password incorrect", %{conn: conn} do
|
||||
secret = TOTP.generate_secret()
|
||||
code = TOTP.generate_token(secret)
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
backup_codes: ["1", "2", "3"],
|
||||
totp: %Settings.TOTP{secret: secret}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code})
|
||||
|> json_response(422)
|
||||
|
||||
settings = refresh_record(user).multi_factor_authentication_settings
|
||||
refute settings.enabled
|
||||
refute settings.totp.confirmed
|
||||
assert settings.backup_codes == ["1", "2", "3"]
|
||||
assert response == %{"error" => "Invalid password."}
|
||||
end
|
||||
|
||||
test "returns error if code incorrect", %{conn: conn} do
|
||||
secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
backup_codes: ["1", "2", "3"],
|
||||
totp: %Settings.TOTP{secret: secret}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
token2 = insert(:oauth_token, scopes: ["read"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
|
||||
|> json_response(422)
|
||||
|
||||
settings = refresh_record(user).multi_factor_authentication_settings
|
||||
refute settings.enabled
|
||||
refute settings.totp.confirmed
|
||||
assert settings.backup_codes == ["1", "2", "3"]
|
||||
assert response == %{"error" => "invalid_token"}
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"})
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: write:security."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/pleroma/accounts/mfa/totp" do
|
||||
test "returns success result", %{conn: conn} do
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %Settings{
|
||||
backup_codes: ["1", "2", "3"],
|
||||
totp: %Settings.TOTP{secret: "secret"}
|
||||
}
|
||||
)
|
||||
|
||||
token = insert(:oauth_token, scopes: ["write", "follow"], user: user)
|
||||
token2 = insert(:oauth_token, scopes: ["read"])
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
|
||||
|> json_response(:ok)
|
||||
|
||||
settings = refresh_record(user).multi_factor_authentication_settings
|
||||
refute settings.enabled
|
||||
assert settings.totp.secret == nil
|
||||
refute settings.totp.confirmed
|
||||
|
||||
assert conn
|
||||
|> put_req_header("authorization", "Bearer #{token2.token}")
|
||||
|> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"})
|
||||
|> json_response(403) == %{
|
||||
"error" => "Insufficient permissions: write:security."
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,11 +6,14 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
|
|||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.MFA.TOTP
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Pleroma.Factory
|
||||
import Ecto.Query
|
||||
|
||||
setup do
|
||||
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
|
@ -160,6 +163,119 @@ test "returns success result when user already in followers", %{conn: conn} do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do
|
||||
test "render the MFA login form", %{conn: conn} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
user2 = insert(:user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post(remote_follow_path(conn, :do_follow), %{
|
||||
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
|
||||
})
|
||||
|> response(200)
|
||||
|
||||
mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id))
|
||||
|
||||
assert response =~ "Two-factor authentication"
|
||||
assert response =~ "Authentication code"
|
||||
assert response =~ mfa_token.token
|
||||
refute user2.follower_address in User.following(user)
|
||||
end
|
||||
|
||||
test "returns error when password is incorrect", %{conn: conn} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
user2 = insert(:user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post(remote_follow_path(conn, :do_follow), %{
|
||||
"authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id}
|
||||
})
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "Wrong username or password"
|
||||
refute user2.follower_address in User.following(user)
|
||||
end
|
||||
|
||||
test "follows", %{conn: conn} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, %{token: token}} = MFA.Token.create_token(user)
|
||||
|
||||
user2 = insert(:user)
|
||||
otp_token = TOTP.generate_token(otp_secret)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> post(
|
||||
remote_follow_path(conn, :do_follow),
|
||||
%{
|
||||
"mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) == "/users/#{user2.id}"
|
||||
assert user2.follower_address in User.following(user)
|
||||
end
|
||||
|
||||
test "returns error when auth code is incorrect", %{conn: conn} do
|
||||
otp_secret = TOTP.generate_secret()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
multi_factor_authentication_settings: %MFA.Settings{
|
||||
enabled: true,
|
||||
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
|
||||
}
|
||||
)
|
||||
|
||||
{:ok, %{token: token}} = MFA.Token.create_token(user)
|
||||
|
||||
user2 = insert(:user)
|
||||
otp_token = TOTP.generate_token(TOTP.generate_secret())
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post(
|
||||
remote_follow_path(conn, :do_follow),
|
||||
%{
|
||||
"mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
|
||||
}
|
||||
)
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "Wrong authentication code"
|
||||
refute user2.follower_address in User.following(user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /ostatus_subscribe - follow/2 without assigned user " do
|
||||
test "follows", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
|
Loading…
Reference in a new issue