Pleroma.Web.TwitterAPI.TwoFactorAuthenticationController -> Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController
This commit is contained in:
parent
68a126317d
commit
3d0c567fbc
49 changed files with 2184 additions and 34 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue