diff options
Diffstat (limited to 'lib/pleroma/web/auth')
| -rw-r--r-- | lib/pleroma/web/auth/authenticator.ex | 13 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/helpers.ex | 33 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/ldap_authenticator.ex | 129 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/pleroma_authenticator.ex | 104 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/totp_authenticator.ex | 45 | ||||
| -rw-r--r-- | lib/pleroma/web/auth/wrapper_authenticator.ex | 42 |
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 |
