{{ $t("There will be no way to recover your data.") }}
-
+
{{ $t("Please enter your password to confirm this action.") }}
@@ -131,9 +137,10 @@ import RouteName from "../../router/name";
import { IConfig } from "../../types/config.model";
import { CONFIG } from "../../graphql/config";
import Subtitle from "../../components/Utils/Subtitle.vue";
+import AuthProviders from "../../components/User/AuthProviders.vue";
@Component({
- components: { Subtitle },
+ components: { Subtitle, AuthProviders },
metaInfo() {
return {
// if no subcomponents specify a metaInfo.title, this title will be used
diff --git a/js/src/views/User/Validate.vue b/js/src/views/User/Validate.vue
index c7a7ef183..f6df0fe0f 100644
--- a/js/src/views/User/Validate.vue
+++ b/js/src/views/User/Validate.vue
@@ -18,7 +18,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
import RouteName from "../../router/name";
-import { saveUserData, changeIdentity } from "../../utils/auth";
+import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
import { ILogin } from "../../types/login.model";
import { ICurrentUserRole } from "../../types/current-user.model";
@@ -45,6 +45,7 @@ export default class Validate extends Vue {
if (data) {
saveUserData(data.validateUser);
+ saveTokenData(data.validateUser);
const { user } = data.validateUser;
diff --git a/lib/graphql/resolvers/config.ex b/lib/graphql/resolvers/config.ex
index 0e6aadf4f..21f536f7e 100644
--- a/lib/graphql/resolvers/config.ex
+++ b/lib/graphql/resolvers/config.ex
@@ -124,7 +124,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
},
rules: Config.instance_rules(),
version: Config.instance_version(),
- federating: Config.instance_federating()
+ federating: Config.instance_federating(),
+ auth: %{
+ ldap: Config.ldap_enabled?(),
+ oauth_providers: Config.oauth_consumer_strategies()
+ }
}
end
end
diff --git a/lib/graphql/resolvers/person.ex b/lib/graphql/resolvers/person.ex
index e6446f185..472d86669 100644
--- a/lib/graphql/resolvers/person.ex
+++ b/lib/graphql/resolvers/person.ex
@@ -202,10 +202,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
"""
def register_person(_parent, args, _resolution) do
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email),
- {:no_actor, nil} <- {:no_actor, Users.get_actor_for_user(user)},
+ user_actor <- Users.get_actor_for_user(user),
+ no_actor <- is_nil(user_actor),
+ {:no_actor, true} <- {:no_actor, no_actor},
args <- Map.put(args, :user_id, user.id),
args <- save_attached_pictures(args),
- {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
+ {:ok, %Actor{} = new_person} <- Actors.new_person(args, true) do
{:ok, new_person}
else
{:error, :user_not_found} ->
diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex
index 7c2cd5ec4..b3043b97c 100644
--- a/lib/graphql/resolvers/user.ex
+++ b/lib/graphql/resolvers/user.ex
@@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
alias Mobilizon.Actors.Actor
alias Mobilizon.Crypto
alias Mobilizon.Federation.ActivityPub
+ alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User}
@@ -59,18 +60,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
Login an user. Returns a token and the user
"""
def login_user(_parent, %{email: email, password: password}, _resolution) do
- with {:ok, %User{confirmed_at: %DateTime{}} = user} <- Users.get_user_by_email(email),
- {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
- Users.authenticate(%{user: user, password: password}) do
- {:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
- else
- {:ok, %User{confirmed_at: nil} = _user} ->
- {:error, "User account not confirmed"}
+ case Authenticator.authenticate(email, password) do
+ {:ok,
+ %{access_token: _access_token, refresh_token: _refresh_token, user: _user} =
+ user_and_tokens} ->
+ {:ok, user_and_tokens}
{:error, :user_not_found} ->
{:error, "No user with this email was found"}
- {:error, :unauthorized} ->
+ {:error, _error} ->
{:error, "Impossible to authenticate, either your email or password are invalid."}
end
end
@@ -82,7 +81,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
{:ok, _old, {exchanged_token, _claims}} <-
Auth.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
- {:ok, refresh_token} <- Users.generate_refresh_token(user) do
+ {:ok, refresh_token} <- Authenticator.generate_refresh_token(user) do
{:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
else
{:error, message} ->
@@ -151,7 +150,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:check_confirmation_token, Email.User.check_confirmation_token(token)},
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
- Users.generate_tokens(user) do
+ Authenticator.generate_tokens(user) do
{:ok,
%{
access_token: access_token,
@@ -192,10 +191,15 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def send_reset_password(_parent, args, _resolution) do
with email <- Map.get(args, :email),
{:ok, %User{locale: locale} = user} <- Users.get_user_by_email(email, true),
+ {:can_reset_password, true} <-
+ {:can_reset_password, Authenticator.can_reset_password?(user)},
{:ok, %Bamboo.Email{} = _email_html} <-
Email.User.send_password_reset_email(user, Map.get(args, :locale, locale)) do
{:ok, email}
else
+ {:can_reset_password, false} ->
+ {:error, "This user can't reset their password"}
+
{:error, :user_not_found} ->
# TODO : implement rate limits for this endpoint
{:error, "No user with this email was found"}
@@ -209,10 +213,10 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
Reset the password from an user
"""
def reset_password(_parent, %{password: password, token: token}, _resolution) do
- with {:ok, %User{} = user} <-
+ with {:ok, %User{email: email} = user} <-
Email.User.check_reset_password_token(password, token),
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
- Users.authenticate(%{user: user, password: password}) do
+ Authenticator.authenticate(email, password) do
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
end
end
@@ -295,10 +299,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def change_password(
_parent,
%{old_password: old_password, new_password: new_password},
- %{context: %{current_user: %User{password_hash: old_password_hash} = user}}
+ %{context: %{current_user: %User{} = user}}
) do
- with {:current_password, true} <-
- {:current_password, Argon2.verify_pass(old_password, old_password_hash)},
+ with {:can_change_password, true} <-
+ {:can_change_password, Authenticator.can_change_password?(user)},
+ {:current_password, {:ok, %User{}}} <-
+ {:current_password, Authenticator.login(user.email, old_password)},
{:same_password, false} <- {:same_password, old_password == new_password},
{:ok, %User{} = user} <-
user
@@ -306,7 +312,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|> Repo.update() do
{:ok, user}
else
- {:current_password, false} ->
+ {:current_password, _} ->
{:error, "The current password is invalid"}
{:same_password, true} ->
@@ -323,10 +329,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end
def change_email(_parent, %{email: new_email, password: password}, %{
- context: %{current_user: %User{email: old_email, password_hash: password_hash} = user}
+ context: %{current_user: %User{email: old_email} = user}
}) do
- with {:current_password, true} <-
- {:current_password, Argon2.verify_pass(password, password_hash)},
+ with {:can_change_password, true} <-
+ {:can_change_password, Authenticator.can_change_email?(user)},
+ {:current_password, {:ok, %User{}}} <-
+ {:current_password, Authenticator.login(user.email, password)},
{:same_email, false} <- {:same_email, new_email == old_email},
{:email_valid, true} <- {:email_valid, Email.Checker.valid?(new_email)},
{:ok, %User{} = user} <-
@@ -347,7 +355,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:ok, user}
else
- {:current_password, false} ->
+ {:current_password, _} ->
{:error, "The password provided is invalid"}
{:same_email, true} ->
@@ -377,14 +385,24 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end
end
- def delete_account(_parent, %{password: password}, %{
- context: %{current_user: %User{password_hash: password_hash} = user}
+ def delete_account(_parent, args, %{
+ context: %{current_user: %User{email: email} = user}
}) do
- case {:current_password, Argon2.verify_pass(password, password_hash)} do
- {:current_password, true} ->
+ with {:user_has_password, true} <- {:user_has_password, Authenticator.has_password?(user)},
+ {:confirmation_password, password} when not is_nil(password) <-
+ {:confirmation_password, Map.get(args, :password)},
+ {:current_password, {:ok, _}} <-
+ {:current_password, Authenticator.authenticate(email, password)} do
+ do_delete_account(user)
+ else
+ # If the user hasn't got any password (3rd-party auth)
+ {:user_has_password, false} ->
do_delete_account(user)
- {:current_password, false} ->
+ {:confirmation_password, nil} ->
+ {:error, "The password provided is invalid"}
+
+ {:current_password, _} ->
{:error, "The password provided is invalid"}
end
end
diff --git a/lib/graphql/schema/config.ex b/lib/graphql/schema/config.ex
index b9cbf36da..e8070cbe6 100644
--- a/lib/graphql/schema/config.ex
+++ b/lib/graphql/schema/config.ex
@@ -39,6 +39,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
end
field(:rules, :string, description: "The instance's rules")
+ field(:auth, :auth, description: "The instance auth methods")
end
object :terms do
@@ -132,6 +133,16 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:groups, :boolean)
end
+ object :auth do
+ field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled")
+ field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers")
+ end
+
+ object :oauth_provider do
+ field(:id, :string, description: "The provider ID")
+ field(:label, :string, description: "The label for the auth provider")
+ end
+
object :config_queries do
@desc "Get the instance config"
field :config, :config do
diff --git a/lib/graphql/schema/user.ex b/lib/graphql/schema/user.ex
index a41317b7d..511f94f45 100644
--- a/lib/graphql/schema/user.ex
+++ b/lib/graphql/schema/user.ex
@@ -52,6 +52,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field(:locale, :string, description: "The user's locale")
+ field(:provider, :string, description: "The user's login provider")
+
field(:disabled, :boolean, description: "Whether the user is disabled")
field(:participations, :paginated_participant_list,
diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex
index 64dc74603..757b0eedc 100644
--- a/lib/mobilizon/actors/actors.ex
+++ b/lib/mobilizon/actors/actors.ex
@@ -13,6 +13,7 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Media.File
alias Mobilizon.Service.Workers
alias Mobilizon.Storage.{Page, Repo}
+ alias Mobilizon.Users
alias Mobilizon.Federation.ActivityPub
@@ -189,14 +190,19 @@ defmodule Mobilizon.Actors do
Creates a new person actor.
"""
@spec new_person(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
- def new_person(args) do
+ def new_person(args, default_actor \\ false) do
args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key())
- with {:ok, %Actor{} = person} <-
+ with {:ok, %Actor{id: person_id} = person} <-
%Actor{}
|> Actor.registration_changeset(args)
|> Repo.insert() do
- Events.create_feed_token(%{user_id: args["user_id"], actor_id: person.id})
+ Events.create_feed_token(%{user_id: args.user_id, actor_id: person.id})
+
+ if default_actor do
+ user = Users.get_user!(args.user_id)
+ Users.update_user(user, %{default_actor_id: person_id})
+ end
{:ok, person}
end
diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex
index 2b8ab92ce..77e1ffadb 100644
--- a/lib/mobilizon/config.ex
+++ b/lib/mobilizon/config.ex
@@ -186,6 +186,24 @@ defmodule Mobilizon.Config do
def anonymous_reporting?,
do: Application.get_env(:mobilizon, :anonymous)[:reports][:allowed]
+ @spec oauth_consumer_strategies() :: list({atom(), String.t()})
+ def oauth_consumer_strategies do
+ [:auth, :oauth_consumer_strategies]
+ |> get([])
+ |> Enum.map(fn strategy ->
+ case strategy do
+ {id, label} when is_atom(id) -> %{id: id, label: label}
+ id when is_atom(id) -> %{id: id, label: nil}
+ end
+ end)
+ end
+
+ @spec oauth_consumer_enabled? :: boolean()
+ def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
+
+ @spec ldap_enabled? :: boolean()
+ def ldap_enabled?, do: get([:ldap, :enabled], false)
+
def instance_resource_providers do
types = get_in(Application.get_env(:mobilizon, Mobilizon.Service.ResourceProviders), [:types])
diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex
index 31a493a86..51b737626 100644
--- a/lib/mobilizon/users/user.ex
+++ b/lib/mobilizon/users/user.ex
@@ -40,14 +40,18 @@ defmodule Mobilizon.Users.User do
:confirmation_token,
:reset_password_sent_at,
:reset_password_token,
+ :default_actor_id,
:locale,
:unconfirmed_email,
- :disabled
+ :disabled,
+ :provider
]
@attrs @required_attrs ++ @optional_attrs
@registration_required_attrs @required_attrs ++ [:password]
+ @auth_provider_required_attrs @required_attrs ++ [:provider]
+
@password_change_required_attrs [:password]
@password_reset_required_attrs @password_change_required_attrs ++
[:reset_password_token, :reset_password_sent_at]
@@ -67,6 +71,7 @@ defmodule Mobilizon.Users.User do
field(:unconfirmed_email, :string)
field(:locale, :string, default: "en")
field(:disabled, :boolean, default: false)
+ field(:provider, :string)
belongs_to(:default_actor, Actor)
has_many(:actors, Actor)
@@ -116,6 +121,16 @@ defmodule Mobilizon.Users.User do
)
end
+ @doc false
+ @spec auth_provider_changeset(t, map) :: Ecto.Changeset.t()
+ def auth_provider_changeset(%__MODULE__{} = user, attrs) do
+ user
+ |> changeset(attrs)
+ |> cast_assoc(:default_actor)
+ |> put_change(:confirmed_at, DateTime.utc_now() |> DateTime.truncate(:second))
+ |> validate_required(@auth_provider_required_attrs)
+ end
+
@doc false
@spec send_password_reset_changeset(t, map) :: Ecto.Changeset.t()
def send_password_reset_changeset(%__MODULE__{} = user, attrs) do
diff --git a/lib/mobilizon/users/users.ex b/lib/mobilizon/users/users.ex
index 0ee54fd09..21bc20c9b 100644
--- a/lib/mobilizon/users/users.ex
+++ b/lib/mobilizon/users/users.ex
@@ -15,13 +15,6 @@ defmodule Mobilizon.Users do
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User}
- alias Mobilizon.Web.Auth
-
- @type tokens :: %{
- required(:access_token) => String.t(),
- required(:refresh_token) => String.t()
- }
-
defenum(UserRole, :user_role, [:administrator, :moderator, :user])
defenum(NotificationPendingNotificationDelay, none: 0, direct: 1, one_hour: 5, one_day: 10)
@@ -41,6 +34,18 @@ defmodule Mobilizon.Users do
end
end
+ @spec create_external(String.t(), String.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def create_external(email, provider) do
+ with {:ok, %User{} = user} <-
+ %User{}
+ |> User.auth_provider_changeset(%{email: email, provider: provider})
+ |> Repo.insert() do
+ Events.create_feed_token(%{user_id: user.id})
+
+ {:ok, user}
+ end
+ end
+
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the user does not exist.
@@ -75,6 +80,16 @@ defmodule Mobilizon.Users do
end
end
+ @doc """
+ Gets an user by its email.
+ """
+ @spec get_user_by_email!(String.t(), boolean | nil) :: User.t()
+ def get_user_by_email!(email, activated \\ nil) do
+ email
+ |> user_by_email_query(activated)
+ |> Repo.one!()
+ end
+
@doc """
Get an user by its activation token.
"""
@@ -267,52 +282,6 @@ defmodule Mobilizon.Users do
@spec count_users :: integer
def count_users, do: Repo.one(from(u in User, select: count(u.id)))
- @doc """
- Authenticate an user.
- """
- @spec authenticate(User.t()) :: {:ok, tokens} | {:error, :unauthorized}
- def authenticate(%{user: %User{password_hash: password_hash} = user, password: password}) do
- # Does password match the one stored in the database?
- if Argon2.verify_pass(password, password_hash) do
- {:ok, _tokens} = generate_tokens(user)
- else
- {:error, :unauthorized}
- end
- end
-
- @doc """
- Generates access token and refresh token for an user.
- """
- @spec generate_tokens(User.t()) :: {:ok, tokens}
- def generate_tokens(user) do
- with {:ok, access_token} <- generate_access_token(user),
- {:ok, refresh_token} <- generate_refresh_token(user) do
- {:ok, %{access_token: access_token, refresh_token: refresh_token}}
- end
- end
-
- @doc """
- Generates access token for an user.
- """
- @spec generate_access_token(User.t()) :: {:ok, String.t()}
- def generate_access_token(user) do
- with {:ok, access_token, _claims} <-
- Auth.Guardian.encode_and_sign(user, %{}, token_type: "access") do
- {:ok, access_token}
- end
- end
-
- @doc """
- Generates refresh token for an user.
- """
- @spec generate_refresh_token(User.t()) :: {:ok, String.t()}
- def generate_refresh_token(user) do
- with {:ok, refresh_token, _claims} <-
- Auth.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
- {:ok, refresh_token}
- end
- end
-
@doc """
Gets a settings for an user.
diff --git a/lib/service/auth/authenticator.ex b/lib/service/auth/authenticator.ex
new file mode 100644
index 000000000..15b8cec25
--- /dev/null
+++ b/lib/service/auth/authenticator.ex
@@ -0,0 +1,93 @@
+defmodule Mobilizon.Service.Auth.Authenticator do
+ @moduledoc """
+ Module to handle authentification (currently through database or LDAP)
+ """
+ alias Mobilizon.Users
+ alias Mobilizon.Users.User
+ alias Mobilizon.Web.Auth.Guardian
+
+ @type tokens :: %{
+ required(:access_token) => String.t(),
+ required(:refresh_token) => String.t()
+ }
+
+ @type tokens_with_user :: %{
+ required(:access_token) => String.t(),
+ required(:refresh_token) => String.t(),
+ required(:user) => User.t()
+ }
+
+ def implementation do
+ Mobilizon.Config.get(
+ Mobilizon.Service.Auth.Authenticator,
+ Mobilizon.Service.Auth.MobilizonAuthenticator
+ )
+ end
+
+ @callback login(String.t(), String.t()) :: {:ok, User.t()} | {:error, any()}
+ @spec login(String.t(), String.t()) :: {:ok, User.t()} | {:error, any()}
+ def login(email, password), do: implementation().login(email, password)
+
+ @callback can_change_email?(User.t()) :: boolean
+ def can_change_email?(%User{} = user), do: implementation().can_change_email?(user)
+
+ @callback can_change_password?(User.t()) :: boolean
+ def can_change_password?(%User{} = user), do: implementation().can_change_password?(user)
+
+ @spec has_password?(User.t()) :: boolean()
+ def has_password?(%User{provider: provider}), do: is_nil(provider) or provider == "ldap"
+
+ @spec can_reset_password?(User.t()) :: boolean()
+ def can_reset_password?(%User{} = user), do: has_password?(user) && can_change_password?(user)
+
+ @spec authenticate(String.t(), String.t()) :: {:ok, tokens_with_user()}
+ def authenticate(email, password) do
+ with {:ok, %User{} = user} <- login(email, password),
+ {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
+ generate_tokens(user) do
+ {:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
+ end
+ end
+
+ @doc """
+ Generates access token and refresh token for an user.
+ """
+ @spec generate_tokens(User.t()) :: {:ok, tokens}
+ def generate_tokens(user) do
+ with {:ok, access_token} <- generate_access_token(user),
+ {:ok, refresh_token} <- generate_refresh_token(user) do
+ {:ok, %{access_token: access_token, refresh_token: refresh_token}}
+ end
+ end
+
+ @doc """
+ Generates access token for an user.
+ """
+ @spec generate_access_token(User.t()) :: {:ok, String.t()}
+ def generate_access_token(user) do
+ with {:ok, access_token, _claims} <-
+ Guardian.encode_and_sign(user, %{}, token_type: "access") do
+ {:ok, access_token}
+ end
+ end
+
+ @doc """
+ Generates refresh token for an user.
+ """
+ @spec generate_refresh_token(User.t()) :: {:ok, String.t()}
+ def generate_refresh_token(user) do
+ with {:ok, refresh_token, _claims} <-
+ Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
+ {:ok, refresh_token}
+ end
+ end
+
+ @spec fetch_user(String.t()) :: User.t() | {:error, :user_not_found}
+ def fetch_user(nil), do: {:error, :user_not_found}
+
+ def fetch_user(email) when not is_nil(email) do
+ with {:ok, %User{} = user} <- Users.get_user_by_email(email, true) do
+ user
+ end
+ end
+end
diff --git a/lib/service/auth/ldap_authenticator.ex b/lib/service/auth/ldap_authenticator.ex
new file mode 100644
index 000000000..6db154376
--- /dev/null
+++ b/lib/service/auth/ldap_authenticator.ex
@@ -0,0 +1,180 @@
+# Portions of this file are derived from Pleroma:
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mobilizon.Service.Auth.LDAPAuthenticator do
+ @moduledoc """
+ Authenticate Mobilizon users through LDAP accounts
+ """
+ alias Mobilizon.Service.Auth.{Authenticator, MobilizonAuthenticator}
+ alias Mobilizon.Users
+ alias Mobilizon.Users.User
+
+ require Logger
+
+ import Authenticator,
+ only: [fetch_user: 1]
+
+ @behaviour Authenticator
+ @base MobilizonAuthenticator
+
+ @connection_timeout 10_000
+ @search_timeout 10_000
+
+ def login(email, password) do
+ with {:ldap, true} <- {:ldap, Mobilizon.Config.get([:ldap, :enabled])},
+ %User{} = user <- ldap_user(email, password) do
+ {:ok, user}
+ else
+ {:error, {:ldap_connection_error, _}} ->
+ # When LDAP is unavailable, try default authenticator
+ @base.login(email, password)
+
+ {:ldap, _} ->
+ @base.login(email, password)
+
+ error ->
+ error
+ end
+ end
+
+ def can_change_email?(%User{provider: provider}), do: provider != "ldap"
+
+ def can_change_password?(%User{provider: provider}), do: provider != "ldap"
+
+ defp ldap_user(email, password) do
+ ldap = Mobilizon.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
+ ensure_eventual_tls(connection, ldap)
+
+ base = Keyword.get(ldap, :base)
+ uid_field = Keyword.get(ldap, :uid, "cn")
+
+ # We first need to find the LDAP UID/CN for this specif email
+ with uid when is_binary(uid) <- search_user(connection, ldap, base, uid_field, email),
+ # Then we can verify the user's password
+ :ok <- bind_user(connection, base, uid_field, uid, password) do
+ case fetch_user(email) do
+ %User{} = user ->
+ user
+
+ _ ->
+ register_user(email)
+ end
+ else
+ {:error, error} ->
+ {:error, error}
+
+ error ->
+ {:error, error}
+ end
+ after
+ :eldap.close(connection)
+ end
+
+ {:error, error} ->
+ Logger.error("Could not open LDAP connection: #{inspect(error)}")
+ {:error, {:ldap_connection_error, error}}
+ end
+ end
+
+ @spec bind_user(any(), String.t(), String.t(), String.t(), String.t()) ::
+ User.t() | any()
+ defp bind_user(connection, base, uid, field, password) do
+ bind = "#{uid}=#{field},#{base}"
+ Logger.debug("Binding to LDAP with \"#{bind}\"")
+ :eldap.simple_bind(connection, bind, password)
+ end
+
+ @spec search_user(any(), Keyword.t(), String.t(), String.t(), String.t()) ::
+ String.t() | {:error, :ldap_registration_missing_attributes} | any()
+ defp search_user(connection, ldap, base, uid, email) do
+ # We may need to bind before performing the search
+ res =
+ if Keyword.get(ldap, :require_bind_for_search, true) do
+ admin_field = Keyword.get(ldap, :bind_uid)
+ admin_password = Keyword.get(ldap, :bind_password)
+ bind_user(connection, base, uid, admin_field, admin_password)
+ else
+ :ok
+ end
+
+ if res == :ok do
+ do_search_user(connection, base, uid, email)
+ else
+ res
+ end
+ end
+
+ # Search an user by uid to find their CN
+ @spec do_search_user(any(), String.t(), String.t(), String.t()) ::
+ String.t() | {:error, :ldap_registration_missing_attributes} | any()
+ defp do_search_user(connection, base, uid, email) do
+ with {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} <-
+ :eldap.search(connection, [
+ {:base, to_charlist(base)},
+ {:filter, :eldap.equalityMatch(to_charlist("mail"), to_charlist(email))},
+ {:scope, :eldap.wholeSubtree()},
+ {:attributes, [to_charlist(uid)]},
+ {:timeout, @search_timeout}
+ ]),
+ {:uid, {_, [uid]}} <- {:uid, List.keyfind(attributes, to_charlist(uid), 0)} do
+ :erlang.list_to_binary(uid)
+ else
+ {:ok, {:eldap_search_result, [], []}} ->
+ Logger.info("Unable to find user with email #{email}")
+ {:error, :ldap_search_email_not_found}
+
+ {:cn, err} ->
+ Logger.error("Could not find LDAP attribute CN: #{inspect(err)}")
+ {:error, :ldap_searcy_missing_attributes}
+
+ error ->
+ error
+ end
+ end
+
+ @spec register_user(String.t()) :: User.t() | any()
+ defp register_user(email) do
+ case Users.create_external(email, "ldap") do
+ {:ok, %User{} = user} ->
+ user
+
+ error ->
+ error
+ end
+ end
+
+ @spec ensure_eventual_tls(any(), Keyword.t()) :: :ok
+ defp ensure_eventual_tls(connection, ldap) 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
+
+ :ok
+ end
+end
diff --git a/lib/service/auth/mobilizon_authenticator.ex b/lib/service/auth/mobilizon_authenticator.ex
new file mode 100644
index 000000000..bec126901
--- /dev/null
+++ b/lib/service/auth/mobilizon_authenticator.ex
@@ -0,0 +1,39 @@
+defmodule Mobilizon.Service.Auth.MobilizonAuthenticator do
+ @moduledoc """
+ Authenticate Mobilizon users through database accounts
+ """
+ alias Mobilizon.Users.User
+
+ alias Mobilizon.Service.Auth.Authenticator
+
+ import Authenticator,
+ only: [fetch_user: 1]
+
+ @behaviour Authenticator
+
+ def login(email, password) do
+ require Logger
+
+ with {:user, %User{password_hash: password_hash, provider: nil} = user}
+ when not is_nil(password_hash) <-
+ {:user, fetch_user(email)},
+ {:acceptable_password, true} <-
+ {:acceptable_password, not (is_nil(password) || password == "")},
+ {:checkpw, true} <- {:checkpw, Argon2.verify_pass(password, password_hash)} do
+ {:ok, user}
+ else
+ {:user, {:error, :user_not_found}} ->
+ {:error, :user_not_found}
+
+ {:acceptable_password, false} ->
+ {:error, :bad_password}
+
+ {:checkpw, false} ->
+ {:error, :bad_password}
+ end
+ end
+
+ def can_change_email?(%User{provider: provider}), do: is_nil(provider)
+
+ def can_change_password?(%User{provider: provider}), do: is_nil(provider)
+end
diff --git a/lib/web/controllers/auth_controller.ex b/lib/web/controllers/auth_controller.ex
new file mode 100644
index 000000000..9e6c9d62c
--- /dev/null
+++ b/lib/web/controllers/auth_controller.ex
@@ -0,0 +1,82 @@
+defmodule Mobilizon.Web.AuthController do
+ use Mobilizon.Web, :controller
+
+ alias Mobilizon.Service.Auth.Authenticator
+ alias Mobilizon.Users
+ alias Mobilizon.Users.User
+ require Logger
+ plug(:put_layout, false)
+
+ plug(Ueberauth)
+
+ def request(conn, %{"provider" => provider} = _params) do
+ redirect(conn, to: "/login?code=Login Provider not found&provider=#{provider}")
+ end
+
+ def callback(
+ %{assigns: %{ueberauth_failure: fails}} = conn,
+ %{"provider" => provider} = _params
+ ) do
+ Logger.warn("Unable to login user with #{provider} #{inspect(fails)}")
+
+ redirect(conn, to: "/login?code=Error with Login Provider&provider=#{provider}")
+ end
+
+ def callback(
+ %{assigns: %{ueberauth_auth: %Ueberauth.Auth{strategy: strategy} = auth}} = conn,
+ _params
+ ) do
+ email = email_from_ueberauth(auth)
+ [_, _, _, strategy] = strategy |> to_string() |> String.split(".")
+ strategy = String.downcase(strategy)
+
+ user =
+ with {:valid_email, false} <- {:valid_email, is_nil(email) or email == ""},
+ {:error, :user_not_found} <- Users.get_user_by_email(email),
+ {:ok, %User{} = user} <- Users.create_external(email, strategy) do
+ user
+ else
+ {:ok, %User{} = user} ->
+ user
+
+ {:error, error} ->
+ {:error, error}
+
+ error ->
+ {:error, error}
+ end
+
+ with %User{} = user <- user,
+ {:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
+ Authenticator.generate_tokens(user) do
+ Logger.info("Logged-in user \"#{email}\" through #{strategy}")
+
+ render(conn, "callback.html", %{
+ access_token: access_token,
+ refresh_token: refresh_token,
+ user: user
+ })
+ else
+ err ->
+ Logger.warn("Unable to login user \"#{email}\" #{inspect(err)}")
+ redirect(conn, to: "/login?code=Error with Login Provider&provider=#{strategy}")
+ end
+ end
+
+ # Github only give public emails as part of the user profile,
+ # so we explicitely request all user emails and filter on the primary one
+ defp email_from_ueberauth(%Ueberauth.Auth{
+ strategy: Ueberauth.Strategy.Github,
+ extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"emails" => emails}}}
+ })
+ when length(emails) > 0,
+ do: emails |> Enum.find(& &1["primary"]) |> (& &1["email"]).()
+
+ defp email_from_ueberauth(%Ueberauth.Auth{
+ extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"email" => email}}}
+ })
+ when not is_nil(email) and email != "",
+ do: email
+
+ defp email_from_ueberauth(_), do: nil
+end
diff --git a/lib/web/router.ex b/lib/web/router.ex
index d7a565186..725afa2d8 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -150,6 +150,10 @@ defmodule Mobilizon.Web.Router do
get("/groups/me", PageController, :index, as: "my_groups")
get("/interact", PageController, :interact)
+
+ get("/auth/:provider", AuthController, :request)
+ get("/auth/:provider/callback", AuthController, :callback)
+ post("/auth/:provider/callback", AuthController, :callback)
end
scope "/proxy/", Mobilizon.Web do
diff --git a/lib/web/views/auth_view.ex b/lib/web/views/auth_view.ex
new file mode 100644
index 000000000..1f94a3407
--- /dev/null
+++ b/lib/web/views/auth_view.ex
@@ -0,0 +1,29 @@
+defmodule Mobilizon.Web.AuthView do
+ @moduledoc """
+ View for the auth routes
+ """
+
+ use Mobilizon.Web, :view
+ alias Mobilizon.Service.Metadata.Instance
+ alias Phoenix.HTML.Tag
+ import Mobilizon.Web.Views.Utils
+
+ def render("callback.html", %{
+ conn: conn,
+ access_token: access_token,
+ refresh_token: refresh_token,
+ user: %{id: user_id, email: user_email, role: user_role, default_actor_id: user_actor_id}
+ }) do
+ info_tags = [
+ Tag.tag(:meta, name: "auth-access-token", content: access_token),
+ Tag.tag(:meta, name: "auth-refresh-token", content: refresh_token),
+ Tag.tag(:meta, name: "auth-user-id", content: user_id),
+ Tag.tag(:meta, name: "auth-user-email", content: user_email),
+ Tag.tag(:meta, name: "auth-user-role", content: user_role),
+ Tag.tag(:meta, name: "auth-user-actor-id", content: user_actor_id)
+ ]
+
+ tags = Instance.build_tags() ++ info_tags
+ inject_tags(tags, get_locale(conn))
+ end
+end
diff --git a/mix.exs b/mix.exs
index 745298465..9b4cf27e2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -46,6 +46,23 @@ defmodule Mobilizon.Mixfile do
defp elixirc_paths(:dev), do: ["lib", "test/support/factory.ex"]
defp elixirc_paths(_), do: ["lib"]
+ # Specifies OAuth dependencies.
+ defp oauth_deps do
+ oauth_strategy_packages =
+ System.get_env("OAUTH_CONSUMER_STRATEGIES")
+ |> to_string()
+ |> String.split()
+ |> Enum.map(fn strategy_entry ->
+ with [_strategy, dependency] <- String.split(strategy_entry, ":") do
+ dependency
+ else
+ [strategy] -> "ueberauth_#{strategy}"
+ end
+ end)
+
+ for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
+ end
+
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
@@ -104,6 +121,16 @@ defmodule Mobilizon.Mixfile do
{:floki, "~> 0.26.0"},
{:ip_reserved, "~> 0.1.0"},
{:fast_sanitize, "~> 0.1"},
+ {:ueberauth, "~> 0.6"},
+ {:ueberauth_twitter, "~> 0.3"},
+ {:ueberauth_github, "~> 0.7"},
+ {:ueberauth_facebook, "~> 0.8"},
+ {:ueberauth_discord, "~> 0.5"},
+ {:ueberauth_google, "~> 0.9"},
+ {:ueberauth_keycloak_strategy,
+ git: "https://github.com/tcitworld/ueberauth_keycloak.git", branch: "upgrade-deps"},
+ {:ueberauth_gitlab_strategy,
+ git: "https://github.com/tcitworld/ueberauth_gitlab.git", branch: "upgrade-deps"},
# Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]},
@@ -116,7 +143,7 @@ defmodule Mobilizon.Mixfile do
{:credo, "~> 1.4.0", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.4", only: :test},
{:elixir_feed_parser, "~> 2.1.0", only: :test}
- ]
+ ] ++ oauth_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
diff --git a/mix.lock b/mix.lock
index 13a34361a..4949dd0c5 100644
--- a/mix.lock
+++ b/mix.lock
@@ -31,12 +31,13 @@
"elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
+ "esaml": {:git, "git://github.com/wrren/esaml.git", "2cace5778e4323216bcff2085ca9739e42a68a42", [branch: "ueberauth_saml"]},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
"ex_cldr": {:hex, :ex_cldr, "2.16.1", "905b03c38b5fb51668a347f2e6b586bcb2c0816cd98f7d913104872c43cbc61f", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.9", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "006e500769982e57e6f3e32cbc4664345f78b014bb5ff48ddc394d67c86c1a8d"},
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.9.0", "ace1c57ba3850753c9ac6ddb89dc0c9a9e5e1c57ecad587e21c8925ad30a3838", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a4b07773e2a326474f44a6bc51fffbec634859a1bad5cc6e6eb55eba45115541"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.5.0", "e369ae3c1cd5cd20aa20988b153fd2902b4ab08aec63ca8757d7104bdb79f867", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ba16b1df60bcec52c986481bbdfa7cfaec899b610f869d2b3c5a9a8149f67668"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.1", "9439d1c40cfd03c3d8f3f60f5d3e3f2c6eaf0fd714541d687531cce78cfb9909", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "62a2f8d41ec6e789137bbf3ac7c944885a8ef6b7ce475905d056d1805b482427"},
- "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.0", "207843c6ddae802a2b5fd43eb95c4b65eae8a0a876ce23ae4413eb098b222977", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3c6c220e03590f08e2f3cb4f3e0c2e1a78fe56a12229331edb952cbdc67935e1"},
+ "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.1", "dced7ffee69c4830593258b69b294adb4c65cf539e1d8ae0a4de31cfc8aa56a0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c6a4b69ef80b8ffbb6c8fb69a2b365ba542580e0f76a15d8c6ee9142bd1b97ea"},
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
"ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"},
@@ -91,7 +92,10 @@
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
+ "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
+ "oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
"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"},
+ "paddle": {:hex, :paddle, "0.1.4", "3697996d79e3d771d6f7560a23e4bad1ed7b7f7fd3e784f97bc39565963b2b13", [:mix], [], "hexpm", "fc719a9e7c86f319b9f4bf413d6f0f326b0c4930d5bc6630d074598ed38e2143"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"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"},
@@ -110,10 +114,21 @@
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
- "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
+ "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
+ "ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
+ "ueberauth_discord": {:hex, :ueberauth_discord, "0.5.0", "52421277b93fda769b51636e542b5085f3861efdc7fa48ac4bedb6dae0b645e1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "9a3808baf44297e26bd5042ba9ea5398aa60023e054eb9a5ac8a4eacd0467a78"},
+ "ueberauth_facebook": {:hex, :ueberauth_facebook, "0.8.1", "c254be4ab367c276773c2e41d3c0fe343ae118e244afc8d5a4e3e5c438951fdc", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "c2cf210ef45bd20611234ef17517f9d1dff6b31d3fb6ad96789143eb0943f540"},
+ "ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"},
+ "ueberauth_gitlab_strategy": {:git, "https://github.com/tcitworld/ueberauth_gitlab.git", "9fc5d30b5d87ff7cdef293a1c128f25777dcbe59", [branch: "upgrade-deps"]},
+ "ueberauth_google": {:hex, :ueberauth_google, "0.9.0", "e098e1d6df647696b858b0289eae7e4dc8c662abee9e309d64bc115192c51bf5", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5453ba074df7ee14fb5b121bb04a64cda5266cd23b28af8a2fdf02dd40959ab4"},
+ "ueberauth_keycloak": {:git, "https://github.com/tcitworld/ueberauth_keycloak.git", "02447d8a75bd36ba26c17c7b1b8bab3538bb2e7a", [branch: "upgrade-deps"]},
+ "ueberauth_keycloak_strategy": {:git, "https://github.com/tcitworld/ueberauth_keycloak.git", "d892f0f9daf9e0023319b69ac2f7c2c6edff2b14", [branch: "upgrade-deps"]},
+ "ueberauth_saml": {:git, "https://github.com/wrren/ueberauth_saml.git", "dfcb4ae3f509afec0f442ce455c41feacac24511", []},
+ "ueberauth_twitter": {:hex, :ueberauth_twitter, "0.4.0", "4b98620341bc91bac90459093bba093c650823b6e2df35b70255c493c17e9227", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:oauther, "~> 1.1", [hex: :oauther, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "fb29c9047ca263038c0c61f5a0ec8597e8564aba3f2b4cb02704b60205fd4468"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
+ "uuid": {:git, "git://github.com/botsunit/erlang-uuid", "1effbbbd200f9f5d9d5154e81b83fe8e4c3fe714", [branch: "master"]},
"xml_builder": {:hex, :xml_builder, "2.0.0", "371ed27bb63bf0598dbaf3f0c466e5dc7d16cb4ecb68f06a67f953654062e21b", [:mix], [], "hexpm", "baeb5c8d42204bac2b856ffd50e8cda42d63b622984538d18d92733e4e790fbd"},
}
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 6f944ba9d..dbbd40d98 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -1177,12 +1177,12 @@ msgstr "If you didn't request this, please ignore this email."
#, elixir-format, fuzzy
#: lib/web/templates/email/email.text.eex:10
msgid "In the meantime, please consider this the software as not (yet) finished. Read more on the Framasoft blog:"
-msgstr "In the meantime, please consider that the software is not (yet) finished. More information %{a_start}on our blog%{a_end}."
+msgstr "In the meantime, please consider that the software is not (yet) finished. More information on our blog."
#, elixir-format, fuzzy
#: lib/web/templates/email/email.text.eex:9
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of version 1 of the software in the fall of 2020."
-msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of %{b_start}version 1 of the software in the first half of 2020%{b_end}."
+msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020."
#, elixir-format, fuzzy
#: lib/web/templates/email/email.text.eex:7
diff --git a/priv/repo/migrations/20200630123819_add_provider_to_user_and_make_password_mandatory.exs b/priv/repo/migrations/20200630123819_add_provider_to_user_and_make_password_mandatory.exs
new file mode 100644
index 000000000..3eb536019
--- /dev/null
+++ b/priv/repo/migrations/20200630123819_add_provider_to_user_and_make_password_mandatory.exs
@@ -0,0 +1,17 @@
+defmodule Mobilizon.Storage.Repo.Migrations.AddProviderToUserAndMakePasswordMandatory do
+ use Ecto.Migration
+
+ def up do
+ alter table(:users) do
+ add(:provider, :string, null: true)
+ modify(:password_hash, :string, null: true)
+ end
+ end
+
+ def down do
+ alter table(:users) do
+ remove(:provider)
+ modify(:password_hash, :string, null: false)
+ end
+ end
+end
diff --git a/test/graphql/resolvers/participant_test.exs b/test/graphql/resolvers/participant_test.exs
index 5d12cab22..3290c9f1d 100644
--- a/test/graphql/resolvers/participant_test.exs
+++ b/test/graphql/resolvers/participant_test.exs
@@ -991,7 +991,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
}
"""
- clear_config([:anonymous, :participation])
+ setup do: clear_config([:anonymous, :participation])
setup %{conn: conn, actor: actor, user: user} do
Mobilizon.Config.clear_config_cache()
diff --git a/test/graphql/resolvers/report_test.exs b/test/graphql/resolvers/report_test.exs
index 7f7e209ec..7063f3197 100644
--- a/test/graphql/resolvers/report_test.exs
+++ b/test/graphql/resolvers/report_test.exs
@@ -33,7 +33,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
}
"""
- clear_config([:anonymous, :reports])
+ setup do: clear_config([:anonymous, :reports])
setup %{conn: conn} do
Mobilizon.Config.clear_config_cache()
diff --git a/test/graphql/resolvers/user_test.exs b/test/graphql/resolvers/user_test.exs
index 9e8fc06cc..477e9fed3 100644
--- a/test/graphql/resolvers/user_test.exs
+++ b/test/graphql/resolvers/user_test.exs
@@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.{Event, Participant}
+ alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.AbsintheHelpers
@@ -45,8 +46,14 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
}
"""
+ @send_reset_password_mutation """
+ mutation SendResetPassword($email: String!) {
+ sendResetPassword(email: $email)
+ }
+ """
+
@delete_user_account_mutation """
- mutation DeleteAccount($password: String!) {
+ mutation DeleteAccount($password: String) {
deleteAccount (password: $password) {
id
}
@@ -712,45 +719,50 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
end
describe "Resolver: Send reset password" do
- test "test send_reset_password/3 with valid email", context do
- user = insert(:user)
-
- mutation = """
- mutation {
- sendResetPassword(
- email: "#{user.email}"
- )
- }
- """
+ test "test send_reset_password/3 with valid email", %{conn: conn} do
+ %User{email: email} = insert(:user)
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @send_reset_password_mutation,
+ variables: %{email: email}
+ )
- assert json_response(res, 200)["data"]["sendResetPassword"] == user.email
+ assert res["data"]["sendResetPassword"] == email
end
- test "test send_reset_password/3 with invalid email", context do
- mutation = """
- mutation {
- sendResetPassword(
- email: "oh no"
- )
- }
- """
+ test "test send_reset_password/3 with invalid email", %{conn: conn} do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @send_reset_password_mutation,
+ variables: %{email: "not an email"}
+ )
+
+ assert hd(res["errors"])["message"] ==
+ "No user with this email was found"
+ end
+
+ test "test send_reset_password/3 for an LDAP user", %{conn: conn} do
+ {:ok, %User{email: email}} = Users.create_external("some@users.com", "ldap")
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @send_reset_password_mutation,
+ variables: %{email: email}
+ )
- assert hd(json_response(res, 200)["errors"])["message"] ==
- "No user with this email was found"
+ assert hd(res["errors"])["message"] ==
+ "This user can't reset their password"
end
end
describe "Resolver: Reset user's password" do
test "test reset_password/3 with valid email", context do
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
+ Users.update_user(user, %{confirmed_at: DateTime.utc_now()})
%Actor{} = insert(:actor, user: user)
{:ok, _email_sent} = Email.User.send_password_reset_email(user)
%User{reset_password_token: reset_password_token} = Users.get_user!(user.id)
@@ -772,6 +784,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
context.conn
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ assert is_nil(json_response(res, 200)["errors"])
assert json_response(res, 200)["data"]["resetPassword"]["user"]["id"] == to_string(user.id)
end
@@ -829,7 +842,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
end
describe "Resolver: Login a user" do
- test "test login_user/3 with valid credentials", context do
+ test "test login_user/3 with valid credentials", %{conn: conn} do
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
{:ok, %User{} = _user} =
@@ -839,30 +852,18 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
"confirmation_token" => nil
})
- mutation = """
- mutation {
- login(
- email: "#{user.email}",
- password: "#{user.password}",
- ) {
- accessToken,
- refreshToken,
- user {
- id
- }
- }
- }
- """
-
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user.password}
+ )
- assert login = json_response(res, 200)["data"]["login"]
+ assert login = res["data"]["login"]
assert Map.has_key?(login, "accessToken") && not is_nil(login["accessToken"])
end
- test "test login_user/3 with invalid password", context do
+ test "test login_user/3 with invalid password", %{conn: conn} do
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
{:ok, %User{} = _user} =
@@ -872,79 +873,40 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
"confirmation_token" => nil
})
- mutation = """
- mutation {
- login(
- email: "#{user.email}",
- password: "bad password",
- ) {
- accessToken,
- user {
- default_actor {
- preferred_username,
- }
- }
- }
- }
- """
-
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: "bad password"}
+ )
- assert hd(json_response(res, 200)["errors"])["message"] ==
+ assert hd(res["errors"])["message"] ==
"Impossible to authenticate, either your email or password are invalid."
end
- test "test login_user/3 with invalid email", context do
- mutation = """
- mutation {
- login(
- email: "bad email",
- password: "bad password",
- ) {
- accessToken,
- user {
- default_actor {
- preferred_username,
- }
- }
- }
- }
- """
-
+ test "test login_user/3 with invalid email", %{conn: conn} do
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: "bad email", password: "bad password"}
+ )
- assert hd(json_response(res, 200)["errors"])["message"] ==
+ assert hd(res["errors"])["message"] ==
"No user with this email was found"
end
- test "test login_user/3 with unconfirmed user", context do
+ test "test login_user/3 with unconfirmed user", %{conn: conn} do
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
- mutation = """
- mutation {
- login(
- email: "#{user.email}",
- password: "#{user.password}",
- ) {
- accessToken,
- user {
- default_actor {
- preferred_username,
- }
- }
- }
- }
- """
-
res =
- context.conn
- |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user.password}
+ )
- assert hd(json_response(res, 200)["errors"])["message"] == "User account not confirmed"
+ assert hd(res["errors"])["message"] == "No user with this email was found"
end
end
@@ -970,7 +932,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
test "test refresh_token/3 with an appropriate token", context do
user = insert(:user)
- {:ok, refresh_token} = Users.generate_refresh_token(user)
+ {:ok, refresh_token} = Authenticator.generate_refresh_token(user)
mutation = """
mutation {
@@ -1441,6 +1403,18 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
assert is_nil(Events.get_participant(participant_id))
end
+ test "delete_account/3 with 3rd-party auth login", %{conn: conn} do
+ {:ok, %User{} = user} = Users.create_external(@email, "keycloak")
+
+ res =
+ conn
+ |> auth_conn(user)
+ |> AbsintheHelpers.graphql_query(query: @delete_user_account_mutation)
+
+ assert is_nil(res["errors"])
+ assert res["data"]["deleteAccount"]["id"] == to_string(user.id)
+ end
+
test "delete_account/3 with invalid password", %{conn: conn} do
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
diff --git a/test/mobilizon/users/users_test.exs b/test/mobilizon/users/users_test.exs
index 01b1520e2..3f06ae066 100644
--- a/test/mobilizon/users/users_test.exs
+++ b/test/mobilizon/users/users_test.exs
@@ -72,14 +72,6 @@ defmodule Mobilizon.UsersTest do
@email "email@domain.tld"
@password "password"
- test "authenticate/1 checks the user's password" do
- {:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
-
- assert {:ok, _} = Users.authenticate(%{user: user, password: @password})
-
- assert {:error, :unauthorized} ==
- Users.authenticate(%{user: user, password: "bad password"})
- end
test "get_user_by_email/1 finds an user by its email" do
{:ok, %User{email: email} = user} = Users.register(%{email: @email, password: @password})
diff --git a/test/service/auth/authentificator_test.exs b/test/service/auth/authentificator_test.exs
new file mode 100644
index 000000000..e0d768c99
--- /dev/null
+++ b/test/service/auth/authentificator_test.exs
@@ -0,0 +1,34 @@
+defmodule Mobilizon.Service.Auth.AuthenticatorTest do
+ use Mobilizon.DataCase
+
+ alias Mobilizon.Service.Auth.Authenticator
+ alias Mobilizon.Users
+ alias Mobilizon.Users.User
+ import Mobilizon.Factory
+
+ @email "email@domain.tld"
+ @password "password"
+
+ describe "test authentification" do
+ test "authenticate/1 checks the user's password" do
+ {:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
+ Users.update_user(user, %{confirmed_at: DateTime.utc_now()})
+
+ assert {:ok, _} = Authenticator.authenticate(@email, @password)
+
+ assert {:error, :bad_password} ==
+ Authenticator.authenticate(@email, "completely wrong password")
+ end
+ end
+
+ describe "fetch_user/1" do
+ test "returns user by email" do
+ user = insert(:user)
+ assert Authenticator.fetch_user(user.email).id == user.id
+ end
+
+ test "returns nil" do
+ assert Authenticator.fetch_user("email") == {:error, :user_not_found}
+ end
+ end
+end
diff --git a/test/service/auth/ldap_authentificator_test.exs b/test/service/auth/ldap_authentificator_test.exs
new file mode 100644
index 000000000..6168f8594
--- /dev/null
+++ b/test/service/auth/ldap_authentificator_test.exs
@@ -0,0 +1,238 @@
+defmodule Mobilizon.Service.Auth.LDAPAuthenticatorTest do
+ use Mobilizon.Web.ConnCase
+ use Mobilizon.Tests.Helpers
+
+ alias Mobilizon.GraphQL.AbsintheHelpers
+ alias Mobilizon.Service.Auth.{Authenticator, LDAPAuthenticator}
+ alias Mobilizon.Users.User
+ alias Mobilizon.Web.Auth.Guardian
+
+ import Mobilizon.Factory
+ import ExUnit.CaptureLog
+ import Mock
+
+ @skip if !Code.ensure_loaded?(:eldap), do: :skip
+ @admin_password "admin_password"
+
+ setup_all do
+ clear_config([:ldap, :enabled], true)
+ clear_config([:ldap, :bind_uid], "admin")
+ clear_config([:ldap, :bind_password], @admin_password)
+ end
+
+ setup_all do:
+ clear_config(
+ Authenticator,
+ LDAPAuthenticator
+ )
+
+ @login_mutation """
+ mutation Login($email: String!, $password: String!) {
+ login(email: $email, password: $password) {
+ accessToken,
+ refreshToken,
+ user {
+ id
+ }
+ }
+ }
+ """
+
+ describe "login" do
+ @tag @skip
+ test "authorizes the existing user using LDAP credentials", %{conn: conn} do
+ user_password = "testpassword"
+ admin_password = "admin_password"
+ user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
+
+ host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
+ port = Mobilizon.Config.get([:ldap, :port])
+
+ with_mocks [
+ {:eldap, [],
+ [
+ open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
+ simple_bind: fn _connection, _dn, password ->
+ case password do
+ ^admin_password -> :ok
+ ^user_password -> :ok
+ end
+ end,
+ equalityMatch: fn _type, _value -> :ok end,
+ wholeSubtree: fn -> :ok end,
+ search: fn _connection, _options ->
+ {:ok,
+ {:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
+ end,
+ close: fn _connection ->
+ send(self(), :close_connection)
+ :ok
+ end
+ ]}
+ ] do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user_password}
+ )
+
+ assert is_nil(res["error"])
+ assert token = res["data"]["login"]["accessToken"]
+
+ {:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
+
+ assert user_from_token.id == user.id
+ assert_received :close_connection
+ end
+ end
+
+ @tag @skip
+ test "creates a new user after successful LDAP authorization", %{conn: conn} do
+ user_password = "testpassword"
+ admin_password = "admin_password"
+ user = build(:user)
+
+ host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
+ port = Mobilizon.Config.get([:ldap, :port])
+
+ with_mocks [
+ {:eldap, [],
+ [
+ open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
+ simple_bind: fn _connection, _dn, password ->
+ case password do
+ ^admin_password -> :ok
+ ^user_password -> :ok
+ end
+ end,
+ equalityMatch: fn _type, _value -> :ok end,
+ wholeSubtree: fn -> :ok end,
+ search: fn _connection, _options ->
+ {:ok,
+ {:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
+ end,
+ close: fn _connection ->
+ send(self(), :close_connection)
+ :ok
+ end
+ ]}
+ ] do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user_password}
+ )
+
+ assert is_nil(res["error"])
+ assert token = res["data"]["login"]["accessToken"]
+
+ {:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
+
+ assert user_from_token.email == user.email
+ assert_received :close_connection
+ end
+ end
+
+ @tag @skip
+ test "falls back to the default authorization when LDAP is unavailable", %{conn: conn} do
+ user_password = "testpassword"
+ admin_password = "admin_password"
+ user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
+
+ host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
+ port = Mobilizon.Config.get([:ldap, :port])
+
+ with_mocks [
+ {:eldap, [],
+ [
+ open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end,
+ simple_bind: fn _connection, _dn, password ->
+ case password do
+ ^admin_password -> :ok
+ ^user_password -> :ok
+ end
+ end,
+ equalityMatch: fn _type, _value -> :ok end,
+ wholeSubtree: fn -> :ok end,
+ search: fn _connection, _options ->
+ {:ok,
+ {:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
+ end,
+ close: fn _connection ->
+ send(self(), :close_connection)
+ :ok
+ end
+ ]}
+ ] do
+ log =
+ capture_log(fn ->
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user_password}
+ )
+
+ assert is_nil(res["error"])
+ assert token = res["data"]["login"]["accessToken"]
+
+ {:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
+
+ assert user_from_token.email == user.email
+ end)
+
+ assert log =~ "Could not open LDAP connection: 'connect failed'"
+ refute_received :close_connection
+ end
+ end
+
+ @tag @skip
+ test "disallow authorization for wrong LDAP credentials", %{conn: conn} do
+ user_password = "testpassword"
+ user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
+
+ host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
+ port = Mobilizon.Config.get([:ldap, :port])
+
+ with_mocks [
+ {:eldap, [],
+ [
+ open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
+ simple_bind: fn _connection, _dn, _password -> {:error, :invalidCredentials} end,
+ close: fn _connection ->
+ send(self(), :close_connection)
+ :ok
+ end
+ ]}
+ ] do
+ res =
+ conn
+ |> AbsintheHelpers.graphql_query(
+ query: @login_mutation,
+ variables: %{email: user.email, password: user_password}
+ )
+
+ refute is_nil(res["errors"])
+
+ assert assert hd(res["errors"])["message"] ==
+ "Impossible to authenticate, either your email or password are invalid."
+
+ assert_received :close_connection
+ end
+ end
+ end
+
+ describe "can change" do
+ test "password" do
+ assert LDAPAuthenticator.can_change_password?(%User{provider: "ldap"}) == false
+ assert LDAPAuthenticator.can_change_password?(%User{provider: nil}) == true
+ end
+
+ test "email" do
+ assert LDAPAuthenticator.can_change_password?(%User{provider: "ldap"}) == false
+ assert LDAPAuthenticator.can_change_password?(%User{provider: nil}) == true
+ end
+ end
+end
diff --git a/test/service/auth/mobilizon_authentificator_test.exs b/test/service/auth/mobilizon_authentificator_test.exs
new file mode 100644
index 000000000..4019f17b3
--- /dev/null
+++ b/test/service/auth/mobilizon_authentificator_test.exs
@@ -0,0 +1,29 @@
+defmodule Mobilizon.Service.Auth.MobilizonAuthenticatorTest do
+ use Mobilizon.DataCase
+
+ alias Mobilizon.Service.Auth.MobilizonAuthenticator
+ alias Mobilizon.Users.User
+ import Mobilizon.Factory
+
+ setup do
+ password = "testpassword"
+ email = "someone@somewhere.tld"
+ user = insert(:user, email: email, password_hash: Argon2.hash_pwd_salt(password))
+ {:ok, [user: user, email: email, password: password]}
+ end
+
+ test "login", %{email: email, password: password, user: user} do
+ assert {:ok, %User{} = returned_user} = MobilizonAuthenticator.login(email, password)
+ assert returned_user.id == user.id
+ end
+
+ test "login with invalid password", %{email: email} do
+ assert {:error, :bad_password} == MobilizonAuthenticator.login(email, "invalid")
+ assert {:error, :bad_password} == MobilizonAuthenticator.login(email, nil)
+ end
+
+ test "login with no credentials" do
+ assert {:error, :user_not_found} == MobilizonAuthenticator.login("some@email.com", nil)
+ assert {:error, :user_not_found} == MobilizonAuthenticator.login(nil, nil)
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 184b78813..220e7755d 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -18,7 +18,8 @@ defmodule Mobilizon.Factory do
role: :user,
confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
confirmation_sent_at: nil,
- confirmation_token: nil
+ confirmation_token: nil,
+ provider: nil
}
end
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index a58f382d8..ffa59e4a4 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -7,6 +7,7 @@ defmodule Mobilizon.Tests.Helpers do
@moduledoc """
Helpers for use in tests.
"""
+ alias Mobilizon.Config
defmacro clear_config(config_path) do
quote do
@@ -17,11 +18,17 @@ defmodule Mobilizon.Tests.Helpers do
defmacro clear_config(config_path, do: yield) do
quote do
- setup do
- initial_setting = Mobilizon.Config.get(unquote(config_path))
- unquote(yield)
- on_exit(fn -> Mobilizon.Config.put(unquote(config_path), initial_setting) end)
- :ok
+ initial_setting = Config.get(unquote(config_path))
+ unquote(yield)
+ on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
+ :ok
+ end
+ end
+
+ defmacro clear_config(config_path, temp_setting) do
+ quote do
+ clear_config(unquote(config_path)) do
+ Config.put(unquote(config_path), unquote(temp_setting))
end
end
end
diff --git a/test/web/controllers/auth_controller_test.exs b/test/web/controllers/auth_controller_test.exs
new file mode 100644
index 000000000..ca2bad443
--- /dev/null
+++ b/test/web/controllers/auth_controller_test.exs
@@ -0,0 +1,54 @@
+defmodule Mobilizon.Web.AuthControllerTest do
+ use Mobilizon.Web.ConnCase
+ alias Mobilizon.Service.Auth.Authenticator
+ alias Mobilizon.Users.User
+
+ @email "someone@somewhere.tld"
+
+ test "login and registration",
+ %{conn: conn} do
+ conn =
+ conn
+ |> assign(:ueberauth_auth, %Ueberauth.Auth{
+ strategy: Ueberauth.Strategy.Twitter,
+ extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"email" => @email}}}
+ })
+ |> get("/auth/twitter/callback")
+
+ assert html_response(conn, 200) =~ "auth-access-token"
+
+ assert %User{confirmed_at: confirmed_at, email: @email} = Authenticator.fetch_user(@email)
+
+ refute is_nil(confirmed_at)
+ end
+
+ test "on bad provider error", %{
+ conn: conn
+ } do
+ conn =
+ conn
+ |> assign(:ueberauth_failure, %{errors: [%{message: "Some error"}]})
+ |> get("/auth/nothing")
+
+ assert "/login?code=Login Provider not found&provider=nothing" =
+ redirection = redirected_to(conn, 302)
+
+ conn = get(recycle(conn), redirection)
+ assert html_response(conn, 200)
+ end
+
+ test "on authentication error", %{
+ conn: conn
+ } do
+ conn =
+ conn
+ |> assign(:ueberauth_failure, %{errors: [%{message: "Some error"}]})
+ |> get("/auth/twitter/callback")
+
+ assert "/login?code=Error with Login Provider&provider=twitter" =
+ redirection = redirected_to(conn, 302)
+
+ conn = get(recycle(conn), redirection)
+ assert html_response(conn, 200)
+ end
+end