Commit 8bfb84183011a79d6992c3efd63227456f9ef0a1

Thomas de Grivel 2021-12-28T21:56:04

TOTP (Google Authenticator)

diff --git a/lib/kmxgit/user_manager.ex b/lib/kmxgit/user_manager.ex
index ccc2c2e..cef4b22 100644
--- a/lib/kmxgit/user_manager.ex
+++ b/lib/kmxgit/user_manager.ex
@@ -284,6 +284,16 @@ defmodule Kmxgit.UserManager do
     |> Repo.update()
   end
 
+  def verify_user_totp(user = %User{}, token) do
+    User.totp_verify(user, token)
+  end
+
+  def delete_user_totp(user = %User{}) do
+    user
+    |> User.totp_changeset(:delete)
+    |> Repo.update()
+  end
+
   def admin_user_present? do
     if Repo.one(from user in User,
           where: [is_admin: true],
diff --git a/lib/kmxgit/user_manager/user.ex b/lib/kmxgit/user_manager/user.ex
index 081b597..bf99abb 100644
--- a/lib/kmxgit/user_manager/user.ex
+++ b/lib/kmxgit/user_manager/user.ex
@@ -215,10 +215,6 @@ defmodule Kmxgit.UserManager.User do
     user.name || login(user)
   end
 
-  def login(user) do
-    user.slug.slug || raise ArgumentError, "no slug for user"
-  end
-
   def ssh_keys_with_env(user) do
     (user.ssh_keys || "")
     |> String.split("\n")
@@ -241,12 +237,12 @@ defmodule Kmxgit.UserManager.User do
   def login(user) do
     if user do
       if user.slug do
-        user.slug.slug
+        user.slug.slug || raise ArgumentError, "no slug for user !"
       else
-        nil
+        raise ArgumentError, "no slug for user"
       end
     else
-      nil
+      raise ArgumentError, "no slug for nil user"
     end
   end
 
@@ -254,6 +250,10 @@ defmodule Kmxgit.UserManager.User do
     :pot.valid_totp(token, secret, [window: 1, addwindow: 1])
   end
 
+  def totp_changeset(user, :delete) do
+    user
+    |> cast(%{otp_last: 0}, [:otp_last])
+  end
   def totp_changeset(user, params) do
     user
     |> cast(params, [:otp_last])
diff --git a/lib/kmxgit_web/controllers/user_controller.ex b/lib/kmxgit_web/controllers/user_controller.ex
index 800c841..2d29c97 100644
--- a/lib/kmxgit_web/controllers/user_controller.ex
+++ b/lib/kmxgit_web/controllers/user_controller.ex
@@ -79,10 +79,11 @@ defmodule KmxgitWeb.UserController do
     if params["login"] == User.login(current_user) do
       user = current_user
       changeset = UserManager.change_user(user)
+      |> Ecto.Changeset.put_change(:otp_last, "")
       totp_enrolment_qrcode_src = totp_enrolment_qrcode_src(user)
       conn
       |> assign(:changeset, changeset)
-      |> assign(:page_title, gettext("Activate TOTP for user %{login}", login: User.login(user)))
+      |> assign(:page_title, gettext("Enrol TOTP for user %{login}", login: User.login(user)))
       |> assign(:totp_enrolment_qrcode_src, totp_enrolment_qrcode_src)
       |> assign(:user, user)
       |> render("totp.html")
@@ -99,13 +100,13 @@ defmodule KmxgitWeb.UserController do
       case UserManager.update_user_totp(user, params["user"]) do
         {:ok, user} ->
           conn
-          |> put_flash(:info, "2FA (TOTP) was successfuly activated")
+          |> put_flash(:info, "Enroled 2FA (TOTP) successfuly.")
           |> redirect(to: Routes.slug_path(conn, :show, User.login(user)))
         {:error, changeset} ->
           totp_enrolment_qrcode_src = totp_enrolment_qrcode_src(user)
           conn
           |> assign(:changeset, changeset)
-          |> assign(:page_title, gettext("Activate TOTP for user %{login}", login: User.login(user)))
+          |> assign(:page_title, gettext("Enrol TOTP for user %{login}", login: User.login(user)))
           |> assign(:totp_enrolment_qrcode_src, totp_enrolment_qrcode_src)
           |> assign(:user, user)
           |> render("totp.html")
@@ -115,6 +116,26 @@ defmodule KmxgitWeb.UserController do
     end
   end
 
+  def totp_delete(conn, params) do
+    current_user = conn.assigns.current_user
+    if params["login"] == User.login(current_user) do
+      user = current_user
+      case UserManager.delete_user_totp(user) do
+        {:ok, user} ->
+          conn
+          |> put_flash(:info, "Removed 2FA (TOTP) successfuly.")
+          |> redirect(to: Routes.slug_path(conn, :show, User.login(user)))
+        {:error, changeset} ->
+          IO.inspect(changeset)
+          conn
+          |> put_flash(:error, "Failed to remove 2FA (TOTP).")
+          |> redirect(to: Routes.user_path(conn, :edit, User.login(user)))
+      end
+    else
+      not_found(conn)
+    end
+  end
+
   def delete(conn, params) do
     current_user = conn.assigns.current_user
     if params["login"] == current_user.slug.slug do
diff --git a/lib/kmxgit_web/controllers/user_session_controller.ex b/lib/kmxgit_web/controllers/user_session_controller.ex
index e46f9ef..6cf2b9c 100644
--- a/lib/kmxgit_web/controllers/user_session_controller.ex
+++ b/lib/kmxgit_web/controllers/user_session_controller.ex
@@ -9,13 +9,20 @@ defmodule KmxgitWeb.UserSessionController do
   end
 
   def create(conn, %{"user" => user_params}) do
-    %{"login" => login, "password" => password} = user_params
-
+    %{"login" => login, "password" => password, "otp" => otp} = user_params
     if user = UserManager.get_user_by_login_and_password(login, password) do
-      UserAuth.log_in_user(conn, user, user_params)
+      if user.otp_last == 0 || UserManager.verify_user_totp(user, otp) do
+        UserAuth.log_in_user(conn, user, user_params)
+      else
+        conn
+        |> assign(:error_message, "Invalid token")
+        |> render("new.html")
+      end
     else
       # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
-      render(conn, "new.html", error_message: "Invalid email or password")
+      conn
+      |> assign(:error_message, "Invalid email or password")
+      |> render("new.html")
     end
   end
   def create(conn, _params) do
diff --git a/lib/kmxgit_web/router.ex b/lib/kmxgit_web/router.ex
index 53782ec..4f347b9 100644
--- a/lib/kmxgit_web/router.ex
+++ b/lib/kmxgit_web/router.ex
@@ -81,6 +81,7 @@ defmodule KmxgitWeb.Router do
       put "/user/:login", UserController, :update
       get "/user/:login/totp", UserController, :totp
       put "/user/:login/totp", UserController, :totp_update
+      delete "/user/:login/totp", UserController, :totp_delete
       get "/repository/:owner/*slug", RepositoryController, :edit
       put "/repository/:owner/*slug", RepositoryController, :update
     end
diff --git a/lib/kmxgit_web/templates/user/edit.html.heex b/lib/kmxgit_web/templates/user/edit.html.heex
index 3af7fe8..7928466 100644
--- a/lib/kmxgit_web/templates/user/edit.html.heex
+++ b/lib/kmxgit_web/templates/user/edit.html.heex
@@ -120,8 +120,22 @@
   <h2><%= gettext "Two factor authentification (2FA)" %></h2>
 
   <%= if @user.otp_last != 0 do %>
-    <%= gettext "2FA enabled (TOTP)" %>
+    <p>
+      <%= gettext "2FA enabled (TOTP)" %>
+    </p>
+    <p>
+      <%= link gettext("Disable TOTP (Google Authenticator)"), to: Routes.user_path(@conn, :totp_delete, User.login(@user)), method: :delete, data: [confirm: gettext("Are you sure you want to disable TOTP (Google Authenticator) for %{site} ?", site: "kmxgit")], class: "btn btn-danger" %>
+    </p>
   <% else %>
-    <%= link gettext("Enable TOTP (Google Authenticator)"), to: Routes.user_path(@conn, :totp, User.login(@user)), class: "btn btn-danger" %>
+    <p>
+      <%= link gettext("Enable TOTP (Google Authenticator)"), to: Routes.user_path(@conn, :totp, User.login(@user)), class: "btn btn-danger" %>
+    </p>
   <% end %>
+
+  <br />
+
+  <hr/>
+
+  <br/>
+  <br/>
 </div>
diff --git a/lib/kmxgit_web/templates/user_session/new.html.heex b/lib/kmxgit_web/templates/user_session/new.html.heex
index c49ae2a..aaa1edb 100644
--- a/lib/kmxgit_web/templates/user_session/new.html.heex
+++ b/lib/kmxgit_web/templates/user_session/new.html.heex
@@ -20,6 +20,12 @@
       <%= error_tag f, :password %>
     </div>
 
+    <div class="mb-3">
+      <%= label f, :otp, gettext("TOTP (Google Authenticator)"), class: "form-label" %>
+      <%= number_input f, :otp, class: "form-control" %>
+      <%= error_tag f, :otp %>
+    </div>
+
     <div class="mb-3 form-check">
       <%= checkbox f, :remember_me, class: "form-check-input" %>
       <%= label f, :remember_me, "Keep me logged in for 60 days", class: "form-check-label" %>