aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/auth
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/auth')
-rw-r--r--lib/pleroma/web/auth/authenticator.ex13
-rw-r--r--lib/pleroma/web/auth/helpers.ex33
-rw-r--r--lib/pleroma/web/auth/ldap_authenticator.ex129
-rw-r--r--lib/pleroma/web/auth/pleroma_authenticator.ex104
-rw-r--r--lib/pleroma/web/auth/totp_authenticator.ex45
-rw-r--r--lib/pleroma/web/auth/wrapper_authenticator.ex42
6 files changed, 366 insertions, 0 deletions
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
new file mode 100644
index 0000000..a0bd154
--- /dev/null
+++ b/lib/pleroma/web/auth/authenticator.ex
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.Authenticator do
+ @callback get_user(Plug.Conn.t()) :: {:ok, user :: struct()} | {:error, any()}
+ @callback create_from_registration(Plug.Conn.t(), registration :: struct()) ::
+ {:ok, User.t()} | {:error, any()}
+ @callback get_registration(Plug.Conn.t()) :: {:ok, registration :: struct()} | {:error, any()}
+ @callback handle_error(Plug.Conn.t(), any()) :: any()
+ @callback auth_template() :: String.t() | nil
+ @callback oauth_consumer_template() :: String.t() | nil
+end
diff --git a/lib/pleroma/web/auth/helpers.ex b/lib/pleroma/web/auth/helpers.ex
new file mode 100644
index 0000000..02e1f39
--- /dev/null
+++ b/lib/pleroma/web/auth/helpers.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.Helpers do
+ alias Pleroma.User
+
+ @doc "Gets user by nickname or email for auth."
+ @spec fetch_user(String.t()) :: User.t() | nil
+ def fetch_user(name) do
+ User.get_by_nickname_or_email(name)
+ end
+
+ # Gets name and password from conn
+ #
+ @spec fetch_credentials(Plug.Conn.t() | map()) ::
+ {:ok, {name :: any, password :: any}} | {:error, :invalid_credentials}
+ def fetch_credentials(%Plug.Conn{params: params} = _),
+ do: fetch_credentials(params)
+
+ def fetch_credentials(params) do
+ case params do
+ %{"authorization" => %{"name" => name, "password" => password}} ->
+ {:ok, {name, password}}
+
+ %{"grant_type" => "password", "username" => name, "password" => password} ->
+ {:ok, {name, password}}
+
+ _ ->
+ {:error, :invalid_credentials}
+ end
+ end
+end
diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex
new file mode 100644
index 0000000..e8cd449
--- /dev/null
+++ b/lib/pleroma/web/auth/ldap_authenticator.ex
@@ -0,0 +1,129 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.LDAPAuthenticator do
+ alias Pleroma.User
+
+ require Logger
+
+ import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
+
+ @behaviour Pleroma.Web.Auth.Authenticator
+ @base Pleroma.Web.Auth.PleromaAuthenticator
+
+ @connection_timeout 10_000
+ @search_timeout 10_000
+
+ defdelegate get_registration(conn), to: @base
+ defdelegate create_from_registration(conn, registration), to: @base
+ defdelegate handle_error(conn, error), to: @base
+ defdelegate auth_template, to: @base
+ defdelegate oauth_consumer_template, to: @base
+
+ def get_user(%Plug.Conn{} = conn) do
+ with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
+ {:ok, {name, password}} <- fetch_credentials(conn),
+ %User{} = user <- ldap_user(name, password) do
+ {:ok, user}
+ else
+ {:ldap, _} ->
+ @base.get_user(conn)
+
+ error ->
+ error
+ end
+ end
+
+ defp ldap_user(name, password) do
+ ldap = Pleroma.Config.get(:ldap, [])
+ host = Keyword.get(ldap, :host, "localhost")
+ port = Keyword.get(ldap, :port, 389)
+ ssl = Keyword.get(ldap, :ssl, false)
+ sslopts = Keyword.get(ldap, :sslopts, [])
+
+ options =
+ [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
+ if sslopts != [], do: [{:sslopts, sslopts}], else: []
+
+ case :eldap.open([to_charlist(host)], options) do
+ {:ok, connection} ->
+ try do
+ if Keyword.get(ldap, :tls, false) do
+ :application.ensure_all_started(:ssl)
+
+ case :eldap.start_tls(
+ connection,
+ Keyword.get(ldap, :tlsopts, []),
+ @connection_timeout
+ ) do
+ :ok ->
+ :ok
+
+ error ->
+ Logger.error("Could not start TLS: #{inspect(error)}")
+ end
+ end
+
+ bind_user(connection, ldap, name, password)
+ after
+ :eldap.close(connection)
+ end
+
+ {:error, error} ->
+ Logger.error("Could not open LDAP connection: #{inspect(error)}")
+ {:error, {:ldap_connection_error, error}}
+ end
+ end
+
+ defp bind_user(connection, ldap, name, password) do
+ uid = Keyword.get(ldap, :uid, "cn")
+ base = Keyword.get(ldap, :base)
+
+ case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
+ :ok ->
+ case fetch_user(name) do
+ %User{} = user ->
+ user
+
+ _ ->
+ register_user(connection, base, uid, name)
+ end
+
+ error ->
+ error
+ end
+ end
+
+ defp register_user(connection, base, uid, name) do
+ case :eldap.search(connection, [
+ {:base, to_charlist(base)},
+ {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
+ {:scope, :eldap.wholeSubtree()},
+ {:timeout, @search_timeout}
+ ]) do
+ {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
+ params = %{
+ name: name,
+ nickname: name,
+ password: nil
+ }
+
+ params =
+ case List.keyfind(attributes, 'mail', 0) do
+ {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
+ _ -> params
+ end
+
+ changeset = User.register_changeset_ldap(%User{}, params)
+
+ case User.register(changeset) do
+ {:ok, user} -> user
+ error -> error
+ end
+
+ error ->
+ error
+ end
+ end
+end
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
new file mode 100644
index 0000000..09a58eb
--- /dev/null
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -0,0 +1,104 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.PleromaAuthenticator do
+ alias Pleroma.Registration
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
+
+ import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
+
+ @behaviour Pleroma.Web.Auth.Authenticator
+
+ def get_user(%Plug.Conn{} = conn) do
+ with {:ok, {name, password}} <- fetch_credentials(conn),
+ {_, %User{} = user} <- {:user, fetch_user(name)},
+ {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)},
+ {:ok, user} <- AuthenticationPlug.maybe_update_password(user, password) do
+ {:ok, user}
+ else
+ {:error, _reason} = error -> error
+ error -> {:error, error}
+ end
+ end
+
+ @doc """
+ Gets or creates Pleroma.Registration record from Ueberauth assigns.
+ Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response —
+ see [`docs/config.md`](docs/config.md).
+ """
+ def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}),
+ do: {:error, :missing_uid}
+
+ def get_registration(%Plug.Conn{
+ assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
+ }) do
+ registration = Registration.get_by_provider_uid(provider, uid)
+
+ if registration do
+ {:ok, registration}
+ else
+ info = auth.info
+
+ %Registration{}
+ |> Registration.changeset(%{
+ provider: to_string(provider),
+ uid: to_string(uid),
+ info: %{
+ "nickname" => info.nickname,
+ "email" => info.email,
+ "name" => info.name,
+ "description" => info.description
+ }
+ })
+ |> Repo.insert()
+ end
+ end
+
+ def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
+
+ @doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."
+ def create_from_registration(
+ %Plug.Conn{params: %{"authorization" => registration_attrs}},
+ %Registration{} = registration
+ ) do
+ nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
+ email = value([registration_attrs["email"], Registration.email(registration)])
+ name = value([registration_attrs["name"], Registration.name(registration)]) || nickname
+ bio = value([registration_attrs["bio"], Registration.description(registration)]) || ""
+
+ random_password = :crypto.strong_rand_bytes(64) |> Base.encode64()
+
+ with {:ok, new_user} <-
+ User.register_changeset(
+ %User{},
+ %{
+ email: email,
+ nickname: nickname,
+ name: name,
+ bio: bio,
+ password: random_password,
+ password_confirmation: random_password
+ },
+ external: true,
+ confirmed: true
+ )
+ |> Repo.insert(),
+ {:ok, _} <-
+ Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do
+ {:ok, new_user}
+ end
+ end
+
+ defp value(list), do: Enum.find(list, &(to_string(&1) != ""))
+
+ def handle_error(%Plug.Conn{} = _conn, error) do
+ error
+ end
+
+ def auth_template, do: nil
+
+ def oauth_consumer_template, do: nil
+end
diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex
new file mode 100644
index 0000000..4be3641
--- /dev/null
+++ b/lib/pleroma/web/auth/totp_authenticator.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.TOTPAuthenticator do
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.AuthenticationPlug
+
+ @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 -> AuthenticationPlug.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
diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex
new file mode 100644
index 0000000..a077cfa
--- /dev/null
+++ b/lib/pleroma/web/auth/wrapper_authenticator.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Auth.WrapperAuthenticator do
+ @behaviour Pleroma.Web.Auth.Authenticator
+
+ defp implementation do
+ Pleroma.Config.get(
+ Pleroma.Web.Auth.Authenticator,
+ Pleroma.Web.Auth.PleromaAuthenticator
+ )
+ end
+
+ @impl true
+ def get_user(plug), do: implementation().get_user(plug)
+
+ @impl true
+ def create_from_registration(plug, registration),
+ do: implementation().create_from_registration(plug, registration)
+
+ @impl true
+ def get_registration(plug), do: implementation().get_registration(plug)
+
+ @impl true
+ def handle_error(plug, error),
+ do: implementation().handle_error(plug, error)
+
+ @impl true
+ def auth_template do
+ # Note: `config :pleroma, :auth_template, "..."` support is deprecated
+ implementation().auth_template() ||
+ Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) ||
+ "show.html"
+ end
+
+ @impl true
+ def oauth_consumer_template do
+ implementation().oauth_consumer_template() ||
+ Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
+ end
+end