Repository: phoenixframework/phoenix (elixir)

Issue:
sqlite generated code - wrong wording?
### Environment

* Elixir version (elixir -v): 1.17.3
* Phoenix version (mix deps): 1.7.14
* Operating system: MacOS 15.0

### Actual behavior

When generating a new Phoenix app with `--database sqlite3`, a call to `Ecto.Migrator` is added to the list of children.
This has a `:skip?` option that uses this generated function:

```elixir
  defp skip_migrations?() do
    # By default, sqlite migrations are run when using a release
    System.get_env("RELEASE_NAME") != nil
  end
```

The comment and the result are different, by default this returns `true` in a release, which causes the migrations to be skipped.




Pull request description:
align sqlite migration code with comment
fixes #5945 

The wording of the code comment implies the opposite of what the function actually does.
I aligned the code with the comment. Can also do it the other way around if needed. 









Patch:
diff --git a/guides/authentication/mix_phx_gen_auth.md b/guides/authentication/mix_phx_gen_auth.md
index 05b7cd533..86e40255d 100644
--- a/guides/authentication/mix_phx_gen_auth.md
+++ b/guides/authentication/mix_phx_gen_auth.md
@@ -2,7 +2,7 @@
 
 > This guide assumes that you have gone through the [introductory guides](overview.html) and have a Phoenix application [up and running](up_and_running.html).
 
-The `mix phx.gen.auth` command generates a flexible, pre-built authentication system into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve.
+The `mix phx.gen.auth` command generates a flexible, pre-built authentication system based on magic links into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve.
 
 ## Getting started
 
@@ -76,11 +76,10 @@ The generated code ships with an authentication module with a handful of plugs t
 
   * `fetch_current_user` - fetches the current user information if available
   * `require_authenticated_user` - must be invoked after `fetch_current_user` and requires that a current user exists and is authenticated
-  * `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users
+  * `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users (only generated for controller based authentication)
+  * `require_sudo_mode` - used for pages that contain sensitive operations and enforces recent authentication
 
-### Confirmation
-
-The generated functionality ships with an account confirmation mechanism, where users have to confirm their account, typically by email. However, the generated code does not forbid users from using the application if their accounts have not yet been confirmed. You can add this functionality by customizing the `require_authenticated_user` in the `Auth` module to check for the `confirmed_at` field (and any other property you desire).
+There are similar `:on_mount` hooks for LiveView based authentication.
 
 ### Notifiers
 
@@ -102,6 +101,10 @@ If your application is sensitive to enumeration attacks, you need to implement y
 
 Furthermore, if you are concerned about enumeration attacks, beware of timing attacks too. For example, registering a new account typically involves additional work (such as writing to the database, sending emails, etc) compared to when an account already exists. Someone could measure the time taken to execute those additional tasks to enumerate emails. This applies to all endpoints (registration, confirmation, password recovery, etc.) that may send email, in-app notifications, etc.
 
+### Confirmation and credential pre-stuffing attacks
+
+The generated functionality ships with an account confirmation mechanism, where users have to confirm their account, typically by email. Furthermore, to prevent security issues, the generated code does forbid users from using the application if their accounts have not yet been confirmed. If you want to change this behavior, please refer to the ["Mixing magic link and password registration" section](Mix.Tasks.Phx.Gen.Auth.html#module-mixing-magic-link-and-password-registration) of `mix phx.gen.auth`.
+
 ### Case sensitiveness
 
 The email lookup is made to be case-insensitive. Case-insensitive lookups are the default in MySQL and MSSQL. In SQLite3 we use [`COLLATE NOCASE`](https://www.sqlite.org/datatype3.html#collating_sequences) in the column definition to support it. In PostgreSQL, we use the [`citext` extension](https://www.postgresql.org/docs/current/citext.html).
@@ -125,6 +128,8 @@ The following links have more information regarding the motivation and design of
   * The [original `phx_gen_auth` repo][phx_gen_auth repo] (for Phoenix 1.5 applications) - This is a great resource to see discussions around decisions that have been made in earlier versions of the project.
   * [Original pull request on bare Phoenix app][auth PR]
   * [Original design spec](https://github.com/dashbitco/mix_phx_gen_auth_demo/blob/auth/README.md)
+  * [Pull request for migrating LiveView based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/1)
+  * [Pull request for migrating controller based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/2)
 
 [phx_gen_auth repo]: https://github.com/aaronrenner/phx_gen_auth
 [auth PR]: https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1
diff --git a/installer/templates/phx_single/lib/app_name/application.ex b/installer/templates/phx_single/lib/app_name/application.ex
index 65aad283e..e195ff6a4 100644
--- a/installer/templates/phx_single/lib/app_name/application.ex
+++ b/installer/templates/phx_single/lib/app_name/application.ex
@@ -37,6 +37,6 @@ defmodule <%= @app_module %>.Application do
 
   defp skip_migrations?() do
     # By default, sqlite migrations are run when using a release
-    System.get_env("RELEASE_NAME") != nil
+    System.get_env("RELEASE_NAME") == nil
   end<% end %>
 end
diff --git a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
index 9043d6b74..d455ad2cb 100644
--- a/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
+++ b/installer/templates/phx_umbrella/apps/app_name/lib/app_name/application.ex
@@ -23,6 +23,6 @@ defmodule <%= @app_module %>.Application do
 
   defp skip_migrations?() do
     # By default, sqlite migrations are run when using a release
-    System.get_env("RELEASE_NAME") != nil
+    System.get_env("RELEASE_NAME") == nil
   end<% end %>
 end
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 5dcec16f6..737d82d15 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -26,6 +26,30 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   to authentication views without necessarily triggering a new HTTP request
   each time (which would result in a full page load).
 
+  ## Mixing magic link and password registration
+
+  `mix phx.gen.auth` generates email based authentication, which assumes the user who
+  owns the email address has control over the account. Therefore, it is extremely
+  important to void all access tokens once the user confirms their account for the first
+  time, and we do so by revoking all tokens upon confirmation.
+
+  However, if you allow users to create an account with password, you must also
+  require them to be logged in by the time of confirmation, otherwise you may be
+  vulnerable to credential pre-stuffing, as the following attack is possible:
+
+  1. An attacker registers a new account with the email address of their target, anticipating
+     that the target creates an account at a later point in time.
+  2. The attacker sets a password when registering.
+  3. The target registers an account and sees that their email address is already in use.
+  4. The target logs in by magic link, but does not change the existing password.
+  5. The attacker maintains access using the password they previously set.
+
+  This is why the default implementation raises whenever a user tries to log in for the first
+  time by magic link and there is a password set. If you add registration with email and
+  password, then you must require the user to be logged in to confirm their account.
+  If they don't have a password (because it was set by the attacker), then they can set one
+  via a "Forgot your password?"-like workflow.
+
   ## Password hashing
 
   The password hashing mechanism defaults to `bcrypt` for
@@ -167,7 +191,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
       router_scope: router_scope(context),
       web_path_prefix: web_path_prefix(schema),
       test_case_options: test_case_options(ecto_adapter),
-      live?: Keyword.fetch!(context.opts, :live)
+      live?: Keyword.fetch!(context.opts, :live),
+      datetime_module: datetime_module(schema)
     ]
 
     paths = Mix.Phoenix.generator_paths()
@@ -298,34 +323,6 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
             "#{singular}_live",
             "login_test.exs"
           ],
-          "reset_password_live.ex": [
-            web_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "reset_password.ex"
-          ],
-          "reset_password_live_test.exs": [
-            web_test_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "reset_password_test.exs"
-          ],
-          "forgot_password_live.ex": [
-            web_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "forgot_password.ex"
-          ],
-          "forgot_password_live_test.exs": [
-            web_test_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "forgot_password_test.exs"
-          ],
           "settings_live.ex": [web_pre, "live", web_path, "#{singular}_live", "settings.ex"],
           "settings_live_test.exs": [
             web_test_pre,
@@ -347,20 +344,6 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
             web_path,
             "#{singular}_live",
             "confirmation_test.exs"
-          ],
-          "confirmation_instructions_live.ex": [
-            web_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "confirmation_instructions.ex"
-          ],
-          "confirmation_instructions_live_test.exs": [
-            web_test_pre,
-            "live",
-            web_path,
-            "#{singular}_live",
-            "confirmation_instructions_test.exs"
           ]
         ]
 
@@ -368,24 +351,6 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
 
       _ ->
         non_live_files = [
-          "confirmation_html.ex": [controller_pre, "#{singular}_confirmation_html.ex"],
-          "confirmation_new.html.heex": [
-            controller_pre,
-            "#{singular}_confirmation_html",
-            "new.html.heex"
-          ],
-          "confirmation_edit.html.heex": [
-            controller_pre,
-            "#{singular}_confirmation_html",
-            "edit.html.heex"
-          ],
-          "confirmation_controller.ex": [controller_pre, "#{singular}_confirmation_controller.ex"],
-          "confirmation_controller_test.exs": [
-            web_test_pre,
-            "controllers",
-            web_path,
-            "#{singular}_confirmation_controller_test.exs"
-          ],
           "registration_new.html.heex": [
             controller_pre,
             "#{singular}_registration_html",
@@ -399,29 +364,13 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
             "#{singular}_registration_controller_test.exs"
           ],
           "registration_html.ex": [controller_pre, "#{singular}_registration_html.ex"],
-          "reset_password_html.ex": [controller_pre, "#{singular}_reset_password_html.ex"],
-          "reset_password_controller.ex": [
-            controller_pre,
-            "#{singular}_reset_password_controller.ex"
-          ],
-          "reset_password_controller_test.exs": [
-            web_test_pre,
-            "controllers",
-            web_path,
-            "#{singular}_reset_password_controller_test.exs"
-          ],
-          "reset_password_edit.html.heex": [
-            controller_pre,
-            "#{singular}_reset_password_html",
-            "edit.html.heex"
-          ],
-          "reset_password_new.html.heex": [
-            controller_pre,
-            "#{singular}_reset_password_html",
-            "new.html.heex"
-          ],
           "session_html.ex": [controller_pre, "#{singular}_session_html.ex"],
           "session_new.html.heex": [controller_pre, "#{singular}_session_html", "new.html.heex"],
+          "session_confirm.html.heex": [
+            controller_pre,
+            "#{singular}_session_html",
+            "confirm.html.heex"
+          ],
           "settings_html.ex": [web_pre, "controllers", web_path, "#{singular}_settings_html.ex"],
           "settings_controller.ex": [controller_pre, "#{singular}_settings_controller.ex"],
           "settings_edit.html.heex": [
@@ -871,6 +820,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   defp test_case_options(Ecto.Adapters.Postgres), do: ", async: true"
   defp test_case_options(adapter) when is_atom(adapter), do: ""
 
+  defp datetime_module(%{timestamp_type: :naive_datetime}), do: NaiveDateTime
+  defp datetime_module(%{timestamp_type: :utc_datetime}), do: DateTime
+  defp datetime_module(%{timestamp_type: :utc_datetime_usec}), do: DateTime
+
   defp put_live_option(schema) do
     opts =
       case Keyword.fetch(schema.opts, :live) do
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index f20306d71..7cc5b28f8 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -107,7 +107,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     end
 
     {context, schema} = build(args)
-    binding = [context: context, schema: schema]
+    binding = [context: context, schema: schema, primary_key: schema.opts[:primary_key] || :id]
     paths = Mix.Phoenix.generator_paths()
 
     prompt_for_conflicts(context)
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index e9222dacc..a28284d77 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -107,7 +107,13 @@ defmodule Mix.Tasks.Phx.Gen.Html do
     {context, schema} = Gen.Context.build(args)
     Gen.Context.prompt_for_code_injection(context)
 
-    binding = [context: context, schema: schema, inputs: inputs(schema)]
+    binding = [
+      context: context,
+      schema: schema,
+      inputs: inputs(schema),
+      primary_key: schema.opts[:primary_key] || :id
+    ]
+
     paths = Mix.Phoenix.generator_paths()
 
     prompt_for_conflicts(context)
@@ -173,7 +179,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
           scope "/#{schema.web_path}", #{inspect(Module.concat(context.web_module, schema.web_namespace))}, as: :#{schema.web_path} do
             pipe_through :browser
             ...
-            resources "/#{schema.plural}", #{inspect(schema.alias)}Controller
+            resources "/#{schema.plural}", #{inspect(schema.alias)}Controller#{if schema.opts[:primary_key], do: ~s[, param: "#{schema.opts[:primary_key]}"]}
           end
       """)
     else
@@ -181,7 +187,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
 
       Add the resource to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
 
-          resources "/#{schema.plural}", #{inspect(schema.alias)}Controller
+          resources "/#{schema.plural}", #{inspect(schema.alias)}Controller#{if schema.opts[:primary_key], do: ~s[, param: "#{schema.opts[:primary_key]}"]}
       """)
     end
 
diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex
index 97177b493..de5271a53 100644
--- a/lib/mix/tasks/phx.gen.json.ex
+++ b/lib/mix/tasks/phx.gen.json.ex
@@ -107,7 +107,8 @@ defmodule Mix.Tasks.Phx.Gen.Json do
       context: context,
       schema: schema,
       core_components?: Code.ensure_loaded?(Module.concat(context.web_module, "CoreComponents")),
-      gettext?: Code.ensure_loaded?(Module.concat(context.web_module, "Gettext"))
+      gettext?: Code.ensure_loaded?(Module.concat(context.web_module, "Gettext")),
+      primary_key: schema.opts[:primary_key] || :id
     ]
 
     paths = Mix.Phoenix.generator_paths()
@@ -171,7 +172,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do
           scope "/#{schema.web_path}", #{inspect(Module.concat(context.web_module, schema.web_namespace))}, as: :#{schema.web_path} do
             pipe_through :api
             ...
-            resources "/#{schema.plural}", #{inspect(schema.alias)}Controller
+            resources "/#{schema.plural}", #{inspect(schema.alias)}Controller#{if schema.opts[:primary_key], do: ~s[, param: "#{schema.opts[:primary_key]}"]}
           end
       """)
     else
@@ -179,7 +180,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do
 
       Add the resource to the "#{Application.get_env(ctx_app, :generators)[:api_prefix] || "/api"}" scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
 
-          resources "/#{schema.plural}", #{inspect(schema.alias)}Controller, except: [:new, :edit]
+          resources "/#{schema.plural}", #{inspect(schema.alias)}Controller, except: [:new, :edit]#{if schema.opts[:primary_key], do: ~s[, param: "#{schema.opts[:primary_key]}"]}
       """)
     end
 
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index 8ee153586..1fbee085b 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -120,7 +120,13 @@ defmodule Mix.Tasks.Phx.Gen.Live do
 
     Gen.Context.prompt_for_code_injection(context)
 
-    binding = [context: context, schema: schema, inputs: inputs(schema)]
+    binding = [
+      context: context,
+      schema: schema,
+      inputs: inputs(schema),
+      primary_key: schema.opts[:primary_key] || :id
+    ]
+
     paths = Mix.Phoenix.generator_paths()
 
     prompt_for_conflicts(context)
@@ -134,7 +140,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
   defp validate_context!(context) do
     cond do
       context.schema.singular == "form" ->
-        Gen.Context.raise_with_help("cannot use form as the schema name because it conflicts with the LiveView assigns!")
+        Gen.Context.raise_with_help(
+          "cannot use form as the schema name because it conflicts with the LiveView assigns!"
+        )
 
       true ->
         :ok
@@ -281,8 +289,8 @@ defmodule Mix.Tasks.Phx.Gen.Live do
     [
       ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|,
       ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Form, :new\n|,
-      ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|,
-      ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Form, :edit|
+      ~s|live "/#{schema.plural}/:#{schema.opts[:primary_key] || :id}", #{inspect(schema.alias)}Live.Show, :show\n|,
+      ~s|live "/#{schema.plural}/:#{schema.opts[:primary_key] || :id}/edit", #{inspect(schema.alias)}Live.Form, :edit|
     ]
   end
 
diff --git a/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex
index 68f3c82dd..4ca2d9651 100644
--- a/lib/mix/tasks/phx.gen.schema.ex
+++ b/lib/mix/tasks/phx.gen.schema.ex
@@ -168,7 +168,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
     prompt_for_conflicts(schema)
 
     schema
-    |> copy_new_files(paths, schema: schema)
+    |> copy_new_files(paths, schema: schema, primary_key: schema.opts[:primary_key] || :id)
     |> print_shell_instructions()
   end
 
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index a65c01fe6..30d6274f7 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -24,24 +24,34 @@ defmodule <%= inspect auth_module %> do
   so LiveView sessions are identified and automatically
   disconnected on log out. The line can be safely removed
   if you are not using LiveView.
+
+  In case the <%= schema.singular %> re-authenticates for sudo mode,
+  the existing remember_me setting is kept, writing a new remember_me cookie.
   """
   def log_in_<%= schema.singular %>(conn, <%= schema.singular %>, params \\ %{}) do
     token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
     <%= schema.singular %>_return_to = get_session(conn, :<%= schema.singular %>_return_to)
+    remember_me = get_session(conn, :<%= schema.singular %>_remember_me)
 
     conn
     |> renew_session()
     |> put_token_in_session(token)
-    |> maybe_write_remember_me_cookie(token, params)
+    |> maybe_write_remember_me_cookie(token, params, remember_me)
     |> redirect(to: <%= schema.singular %>_return_to || signed_in_path(conn))
   end
 
-  defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
-    put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
-  end
+  defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}, _),
+    do: write_remember_me_cookie(conn, token)
+
+  defp maybe_write_remember_me_cookie(conn, token, _params, true),
+    do: write_remember_me_cookie(conn, token)
 
-  defp maybe_write_remember_me_cookie(conn, _token, _params) do
+  defp maybe_write_remember_me_cookie(conn, _token, _params, _), do: conn
+
+  defp write_remember_me_cookie(conn, token) do
     conn
+    |> put_session(:<%= schema.singular %>_remember_me, true)
+    |> put_resp_cookie(@remember_me_cookie, token, @remember_me_options)
   end
 
   # This function renews the session ID and erases the whole
@@ -124,9 +134,6 @@ defmodule <%= inspect auth_module %> do
       on <%= schema.singular %>_token.
       Redirects to login page if there's no logged <%= schema.singular %>.
 
-    * `:redirect_if_<%= schema.singular %>_is_authenticated` - Authenticates the <%= schema.singular %> from the session.
-      Redirects to signed_in_path if there's a logged <%= schema.singular %>.
-
   ## Examples
 
   Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
@@ -164,13 +171,18 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
-  def on_mount(:redirect_if_<%= schema.singular %>_is_authenticated, _params, session, socket) do
+  def on_mount(:ensure_sudo_mode, _params, session, socket) do
     socket = mount_current_<%= schema.singular %>(socket, session)
 
-    if socket.assigns.current_<%= schema.singular %> do
-      {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
-    else
+    if <%= inspect context.alias %>.sudo_mode?(socket.assigns.current_<%= schema.singular %>, -10) do
       {:cont, socket}
+    else
+      socket =
+        socket
+        |> Phoenix.LiveView.put_flash(:error, "You must re-authenticate to access this page.")
+        |> Phoenix.LiveView.redirect(to: ~p"<%= schema.route_prefix %>/log-in")
+
+      {:halt, socket}
     end
   end
 
@@ -182,7 +194,22 @@ defmodule <%= inspect auth_module %> do
     end)
   end
 
-  <% end %>@doc """
+  <% else %>@doc """
+  Used for routes that require sudo mode.
+  """
+  def require_sudo_mode(conn, _opts) do
+    if <%= inspect context.alias %>.sudo_mode?(conn.assigns.current_<%= schema.singular %>, -10) do
+      conn
+    else
+      conn
+      |> put_flash(:error, "You must re-authenticate to access this page.")
+      |> maybe_store_return_to()
+      |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
+      |> halt()
+    end
+  end
+
+  @doc """
   Used for routes that require the <%= schema.singular %> to not be authenticated.
   """
   def redirect_if_<%= schema.singular %>_is_authenticated(conn, _opts) do
@@ -195,7 +222,7 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
-  @doc """
+  <% end %>@doc """
   Used for routes that require the <%= schema.singular %> to be authenticated.
 
   If you want to enforce the <%= schema.singular %> email is confirmed before
@@ -213,17 +240,38 @@ defmodule <%= inspect auth_module %> do
     end
   end
 
-  defp put_token_in_session(conn, token) do
+  <%= if live? do %>defp put_token_in_session(conn, token) do
     conn
     |> put_session(:<%= schema.singular %>_token, token)
-    |> put_session(:live_socket_id, "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}")
+    |> put_session(:live_socket_id, <%= schema.singular %>_session_topic(token))
+  end
+
+  @doc """
+  Disconnects existing sockets for the given tokens.
+  """
+  def disconnect_sessions(tokens) do
+    Enum.each(tokens, fn %{token: token} ->
+      <%= inspect endpoint_module %>.broadcast(<%= schema.singular %>_session_topic(token), "disconnect", %{})
+    end)
   end
 
-  defp maybe_store_return_to(%{method: "GET"} = conn) do
+  defp <%= schema.singular %>_session_topic(token), do: "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}"
+
+  <% else %>defp put_token_in_session(conn, token) do
+    put_session(conn, :<%= schema.singular %>_token, token)
+  end
+
+  <% end %>defp maybe_store_return_to(%{method: "GET"} = conn) do
     put_session(conn, :<%= schema.singular %>_return_to, current_path(conn))
   end
 
   defp maybe_store_return_to(conn), do: conn
 
-  defp signed_in_path(_conn), do: ~p"/"
+  <%= if live? do %>@doc "Returns the path to redirect to after log in."
+  # the <%= schema.singular %> was already logged in, redirect to settings
+  def signed_in_path(%Plug.Conn{assigns: %{current_<%= schema.singular %>: %<%= inspect context.alias %>.<%= inspect schema.alias %>{}}}) do
+    ~p"<%= schema.route_prefix %>/settings"
+  end
+
+  def signed_in_path(_), do: ~p"/"<% else %>defp signed_in_path(_conn), do: ~p"/"<% end %>
 end
diff --git a/priv/templates/phx.gen.auth/confirmation_controller.ex b/priv/templates/phx.gen.auth/confirmation_controller.ex
deleted file mode 100644
index 5a70d6e06..000000000
--- a/priv/templates/phx.gen.auth/confirmation_controller.ex
+++ /dev/null
@@ -1,56 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationController do
-  use <%= inspect context.web_module %>, :controller
-
-  alias <%= inspect context.module %>
-
-  def new(conn, _params) do
-    render(conn, :new)
-  end
-
-  def create(conn, %{"<%= schema.singular %>" => %{"email" => email}}) do
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
-      <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(
-        <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/confirm/#{&1}")
-      )
-    end
-
-    conn
-    |> put_flash(
-      :info,
-      "If your email is in our system and it has not been confirmed yet, " <>
-        "you will receive an email with instructions shortly."
-    )
-    |> redirect(to: ~p"/")
-  end
-
-  def edit(conn, %{"token" => token}) do
-    render(conn, :edit, token: token)
-  end
-
-  # Do not log in the <%= schema.singular %> after confirmation to avoid a
-  # leaked token giving the <%= schema.singular %> access to the account.
-  def update(conn, %{"token" => token}) do
-    case <%= inspect context.alias %>.confirm_<%= schema.singular %>(token) do
-      {:ok, _} ->
-        conn
-        |> put_flash(:info, "<%= schema.human_singular %> confirmed successfully.")
-        |> redirect(to: ~p"/")
-
-      :error ->
-        # If there is a current <%= schema.singular %> and the account was already confirmed,
-        # then odds are that the confirmation link was already visited, either
-        # by some automation or by the <%= schema.singular %> themselves, so we redirect without
-        # a warning message.
-        case conn.assigns do
-          %{current_<%= schema.singular %>: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
-            redirect(conn, to: ~p"/")
-
-          %{} ->
-            conn
-            |> put_flash(:error, "<%= schema.human_singular %> confirmation link is invalid or it has expired.")
-            |> redirect(to: ~p"/")
-        end
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
deleted file mode 100644
index 0f6d96d41..000000000
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">Confirm account</.header>
-
-  <.simple_form for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
-    <:actions>
-      <.button class="w-full">Confirm my account</.button>
-    </:actions>
-  </.simple_form>
-
-  <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
-    <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-  </p>
-</div>
diff --git a/priv/templates/phx.gen.auth/confirmation_html.ex b/priv/templates/phx.gen.auth/confirmation_html.ex
deleted file mode 100644
index 539b8dc1e..000000000
--- a/priv/templates/phx.gen.auth/confirmation_html.ex
+++ /dev/null
@@ -1,5 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationHTML do
-  use <%= inspect context.web_module %>, :html
-
-  embed_templates "<%= schema.singular %>_confirmation_html/*"
-end
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
deleted file mode 100644
index bf54a9933..000000000
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
+++ /dev/null
@@ -1,57 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructions do
-  use <%= inspect context.web_module %>, :live_view
-
-  alias <%= inspect context.module %>
-
-  def render(assigns) do
-    ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">
-        No confirmation instructions received?
-        <:subtitle>We'll send a new confirmation link to your inbox</:subtitle>
-      </.header>
-
-      <.simple_form for={@form} id="resend_confirmation_form" phx-submit="send_instructions">
-        <.input
-          field={@form[:email]}
-          type="email"
-          placeholder="Email"
-          autocomplete="username"
-          required
-        />
-        <:actions>
-          <.button phx-disable-with="Sending..." class="w-full">
-            Resend confirmation instructions
-          </.button>
-        </:actions>
-      </.simple_form>
-
-      <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
-        <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-      </p>
-    </div>
-    """
-  end
-
-  def mount(_params, _session, socket) do
-    {:ok, assign(socket, form: to_form(%{}, as: "<%= schema.singular %>"))}
-  end
-
-  def handle_event("send_instructions", %{"<%= schema.singular %>" => %{"email" => email}}, socket) do
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
-      <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(
-        <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/confirm/#{&1}")
-      )
-    end
-
-    info =
-      "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
-
-    {:noreply,
-     socket
-     |> put_flash(:info, info)
-     |> redirect(to: ~p"/")}
-  end
-end
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 251afacec..c0fece4f5 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -3,56 +3,73 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   alias <%= inspect context.module %>
 
-  def render(%{live_action: :edit} = assigns) do
+  def render(assigns) do
     ~H"""
     <div class="mx-auto max-w-sm">
-      <.header class="text-center">Confirm Account</.header>
+      <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
 
-      <.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
+      <.simple_form
+        :if={!@<%= schema.singular %>.confirmed_at}
+        for={@form}
+        id="confirmation_form"
+        phx-submit="submit"
+        action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
+        phx-trigger-action={@trigger_submit}
+      >
         <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+        <.input
+          :if={!@current_<%= schema.singular %>}
+          field={@form[:remember_me]}
+          type="checkbox"
+          label="Keep me logged in"
+        />
         <:actions>
           <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
         </:actions>
       </.simple_form>
 
-      <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
-        <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
+      <.simple_form
+        :if={@<%= schema.singular %>.confirmed_at}
+        for={@form}
+        id="login_form"
+        phx-submit="submit"
+        action={~p"<%= schema.route_prefix %>/log-in"}
+        phx-trigger-action={@trigger_submit}
+      >
+        <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+        <.input
+          :if={!@current_<%= schema.singular %>}
+          field={@form[:remember_me]}
+          type="checkbox"
+          label="Keep me logged in"
+        />
+        <:actions>
+          <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
+        </:actions>
+      </.simple_form>
+
+      <p :if={!@<%= schema.singular %>.confirmed_at} class="mt-8 p-4 border text-center">
+        Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
       </p>
     </div>
     """
   end
 
   def mount(%{"token" => token}, _session, socket) do
-    form = to_form(%{"token" => token}, as: "<%= schema.singular %>")
-    {:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
-  end
+    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_magic_link_token(token) do
+      form = to_form(%{"token" => token}, as: "<%= schema.singular %>")
 
-  # Do not log in the <%= schema.singular %> after confirmation to avoid a
-  # leaked token giving the <%= schema.singular %> access to the account.
-  def handle_event("confirm_account", %{"<%= schema.singular %>" => %{"token" => token}}, socket) do
-    case <%= inspect context.alias %>.confirm_<%= schema.singular %>(token) do
-      {:ok, _} ->
-        {:noreply,
-         socket
-         |> put_flash(:info, "<%= inspect schema.alias %> confirmed successfully.")
-         |> redirect(to: ~p"/")}
-
-      :error ->
-        # If there is a current <%= schema.singular %> and the account was already confirmed,
-        # then odds are that the confirmation link was already visited, either
-        # by some automation or by the <%= schema.singular %> themselves, so we redirect without
-        # a warning message.
-        case socket.assigns do
-          %{current_<%= schema.singular %>: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
-            {:noreply, redirect(socket, to: ~p"/")}
-
-          %{} ->
-            {:noreply,
-             socket
-             |> put_flash(:error, "<%= inspect schema.alias %> confirmation link is invalid or it has expired.")
-             |> redirect(to: ~p"/")}
-        end
+      {:ok, assign(socket, <%= schema.singular %>: <%= schema.singular %>, form: form, trigger_submit: false),
+       temporary_assigns: [form: nil]}
+    else
+      {:ok,
+       socket
+       |> put_flash(:error, "Magic link is invalid or it has expired.")
+       |> push_navigate(to: ~p"<%= schema.route_prefix %>/log-in")}
     end
   end
+
+  def handle_event("submit", %{"<%= schema.singular %>" => params}, socket) do
+    {:noreply, assign(socket, form: to_form(params, as: "<%= schema.singular %>"), trigger_submit: true)}
+  end
 end
diff --git a/priv/templates/phx.gen.auth/confirmation_new.html.heex b/priv/templates/phx.gen.auth/confirmation_new.html.heex
deleted file mode 100644
index 7edb1088c..000000000
--- a/priv/templates/phx.gen.auth/confirmation_new.html.heex
+++ /dev/null
@@ -1,20 +0,0 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">
-    No confirmation instructions received?
-    <:subtitle>We'll send a new confirmation link to your inbox</:subtitle>
-  </.header>
-
-  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm"}>
-    <.input field={f[:email]} type="email" placeholder="Email" autocomplete="username" required />
-    <:actions>
-      <.button phx-disable-with="Sending..." class="w-full">
-        Resend confirmation instructions
-      </.button>
-    </:actions>
-  </.simple_form>
-
-  <p :if={!@current_<%= schema.singular %>} class="text-center mt-4">
-    <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-  </p>
-</div>
diff --git a/priv/templates/phx.gen.auth/conn_case.exs b/priv/templates/phx.gen.auth/conn_case.exs
index c3939fd3a..dafec5d16 100644
--- a/priv/templates/phx.gen.auth/conn_case.exs
+++ b/priv/templates/phx.gen.auth/conn_case.exs
@@ -7,9 +7,15 @@
   It stores an updated connection and a registered <%= schema.singular %> in the
   test context.
   """
-  def register_and_log_in_<%= schema.singular %>(%{conn: conn}) do
+  def register_and_log_in_<%= schema.singular %>(%{conn: conn} = context) do
     <%= schema.singular %> = <%= inspect context.module %>Fixtures.<%= schema.singular %>_fixture()
-    %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>), <%= schema.singular %>: <%= schema.singular %>}
+
+    opts =
+      context
+      |> Map.take([:token_inserted_at])
+      |> Enum.into([])
+
+    %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>, opts), <%= schema.singular %>: <%= schema.singular %>}
   end
 
   @doc """
@@ -17,10 +23,18 @@
 
   It returns an updated `conn`.
   """
-  def log_in_<%= schema.singular %>(conn, <%= schema.singular %>) do
+  def log_in_<%= schema.singular %>(conn, <%= schema.singular %>, opts \\ []) do
     token = <%= inspect context.module %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
 
+    maybe_set_token_inserted_at(token, opts[:token_inserted_at])
+
     conn
     |> Phoenix.ConnTest.init_test_session(%{})
     |> Plug.Conn.put_session(:<%= schema.singular %>_token, token)
   end
+
+  defp maybe_set_token_inserted_at(_token, nil), do: nil
+
+  defp maybe_set_token_inserted_at(token, inserted_at) do
+    <%= inspect context.module %>Fixtures.override_token_inserted_at(token, inserted_at)
+  end
diff --git a/priv/templates/phx.gen.auth/context_fixtures_functions.ex b/priv/templates/phx.gen.auth/context_fixtures_functions.ex
index 5a8b1474e..64a2f0c13 100644
--- a/priv/templates/phx.gen.auth/context_fixtures_functions.ex
+++ b/priv/templates/phx.gen.auth/context_fixtures_functions.ex
@@ -1,18 +1,41 @@
+  import Ecto.Query
+
+  alias <%= inspect context.module %>
+
   def unique_<%= schema.singular %>_email, do: "<%= schema.singular %>#{System.unique_integer()}@example.com"
   def valid_<%= schema.singular %>_password, do: "hello world!"
 
   def valid_<%= schema.singular %>_attributes(attrs \\ %{}) do
     Enum.into(attrs, %{
-      email: unique_<%= schema.singular %>_email(),
-      password: valid_<%= schema.singular %>_password()
+      email: unique_<%= schema.singular %>_email()
     })
   end
 
-  def <%= schema.singular %>_fixture(attrs \\ %{}) do
+  def unconfirmed_<%= schema.singular %>_fixture(attrs \\ %{}) do
     {:ok, <%= schema.singular %>} =
       attrs
       |> valid_<%= schema.singular %>_attributes()
-      |> <%= inspect context.module %>.register_<%= schema.singular %>()
+      |> <%= inspect context.alias %>.register_<%= schema.singular %>()
+
+    <%= schema.singular %>
+  end
+
+  def <%= schema.singular %>_fixture(attrs \\ %{}) do
+    <%= schema.singular %> = unconfirmed_<%= schema.singular %>_fixture(attrs)
+
+    token =
+      extract_<%= schema.singular %>_token(fn url ->
+        <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+      end)
+
+    {:ok, <%= schema.singular %>, _expired_tokens} = <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(token)
+
+    <%= schema.singular %>
+  end
+
+  def set_password(<%= schema.singular %>) do
+    {:ok, <%= schema.singular %>, _expired_tokens} =
+      <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, %{password: valid_<%= schema.singular %>_password()})
 
     <%= schema.singular %>
   end
@@ -22,3 +45,18 @@
     [_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
     token
   end
+
+  def override_token_inserted_at(token, inserted_at) when is_binary(token) do
+    <%= inspect schema.repo %>.update_all(
+      from(t in <%= inspect context.alias %>.<%= inspect schema.alias %>Token,
+        where: t.token == ^token
+      ),
+      set: [inserted_at: inserted_at]
+    )
+  end
+
+  def generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>) do
+    {encoded_token, <%= schema.singular %>_token} = <%= inspect context.alias %>.<%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "login")
+    <%= inspect schema.repo %>.insert!(<%= schema.singular %>_token)
+    {encoded_token, <%= schema.singular %>_token.token}
+  end
diff --git a/priv/templates/phx.gen.auth/context_functions.ex b/priv/templates/phx.gen.auth/context_functions.ex
index e3a2c090f..51988d129 100644
--- a/priv/templates/phx.gen.auth/context_functions.ex
+++ b/priv/templates/phx.gen.auth/context_functions.ex
@@ -68,63 +68,45 @@
   """
   def register_<%= schema.singular %>(attrs) do
     %<%= inspect schema.alias %>{}
-    |> <%= inspect schema.alias %>.registration_changeset(attrs)
+    |> <%= inspect schema.alias %>.email_changeset(attrs)
     |> Repo.insert()
   end
 
-  @doc """
-  Returns an `%Ecto.Changeset{}` for tracking <%= schema.singular %> changes.
-
-  ## Examples
+  ## Settings
 
-      iex> change_<%= schema.singular %>_registration(<%= schema.singular %>)
-      %Ecto.Changeset{data: %<%= inspect schema.alias %>{}}
+  @doc """
+  Checks whether the <%= schema.singular %> is in sudo mode.
 
+  The <%= schema.singular %> is in sudo mode when the last authentication was done no further
+  than 20 minutes ago. The limit can be given as second argument in minutes.
   """
-  def change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{} = <%= schema.singular %>, attrs \\ %{}) do
-    <%= inspect schema.alias %>.registration_changeset(<%= schema.singular %>, attrs, hash_password: false, validate_email: false)
+  def sudo_mode?(<%= schema.singular %>, minutes \\ -20)
+
+  def sudo_mode?(%<%= inspect schema.alias %>{authenticated_at: ts}, minutes) when is_struct(ts, <%= inspect datetime_module %>) do
+    <%= inspect datetime_module %>.after?(ts, <%= inspect datetime_module %>.utc_now() |> <%= inspect datetime_module %>.add(minutes, :minute))
   end
 
-  ## Settings
+  def sudo_mode?(_<%= schema.singular %>, _minutes), do: false
 
   @doc """
   Returns an `%Ecto.Changeset{}` for changing the <%= schema.singular %> email.
 
+  See `<%= inspect context.module %>.<%= inspect schema.alias %>.email_changeset/3` for a list of supported options.
+
   ## Examples
 
       iex> change_<%= schema.singular %>_email(<%= schema.singular %>)
       %Ecto.Changeset{data: %<%= inspect schema.alias %>{}}
 
   """
-  def change_<%= schema.singular %>_email(<%= schema.singular %>, attrs \\ %{}) do
-    <%= inspect schema.alias %>.email_changeset(<%= schema.singular %>, attrs, validate_email: false)
-  end
-
-  @doc """
-  Emulates that the email will change without actually changing
-  it in the database.
-
-  ## Examples
-
-      iex> apply_<%= schema.singular %>_email(<%= schema.singular %>, "valid password", %{email: ...})
-      {:ok, %<%= inspect schema.alias %>{}}
-
-      iex> apply_<%= schema.singular %>_email(<%= schema.singular %>, "invalid password", %{email: ...})
-      {:error, %Ecto.Changeset{}}
-
-  """
-  def apply_<%= schema.singular %>_email(<%= schema.singular %>, password, attrs) do
-    <%= schema.singular %>
-    |> <%= inspect schema.alias %>.email_changeset(attrs)
-    |> <%= inspect schema.alias %>.validate_current_password(password)
-    |> Ecto.Changeset.apply_action(:update)
+  def change_<%= schema.singular %>_email(<%= schema.singular %>, attrs \\ %{}, opts \\ []) do
+    <%= inspect schema.alias %>.email_changeset(<%= schema.singular %>, attrs, opts)
   end
 
   @doc """
   Updates the <%= schema.singular %> email using the given token.
 
   If the token matches, the <%= schema.singular %> email is updated and the token is deleted.
-  The confirmed_at date is also updated to the current time.
   """
   def update_<%= schema.singular %>_email(<%= schema.singular %>, token) do
     context = "change:#{<%= schema.singular %>.email}"
@@ -139,70 +121,48 @@
   end
 
   defp <%= schema.singular %>_email_multi(<%= schema.singular %>, email, context) do
-    changeset =
-      <%= schema.singular %>
-      |> <%= inspect schema.alias %>.email_changeset(%{email: email})
-      |> <%= inspect schema.alias %>.confirm_changeset()
+    changeset = <%= inspect schema.alias %>.email_changeset(<%= schema.singular %>, %{email: email})
 
     Ecto.Multi.new()
     |> Ecto.Multi.update(:<%= schema.singular %>, changeset)
     |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [context]))
   end
 
-  @doc ~S"""
-  Delivers the update email instructions to the given <%= schema.singular %>.
-
-  ## Examples
-
-      iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}"))
-      {:ok, %{to: ..., body: ...}}
-
-  """
-  def deliver_<%= schema.singular %>_update_email_instructions(%<%= inspect schema.alias %>{} = <%= schema.singular %>, current_email, update_email_url_fun)
-      when is_function(update_email_url_fun, 1) do
-    {encoded_token, <%= schema.singular %>_token} = <%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "change:#{current_email}")
-
-    Repo.insert!(<%= schema.singular %>_token)
-    <%= inspect schema.alias %>Notifier.deliver_update_email_instructions(<%= schema.singular %>, update_email_url_fun.(encoded_token))
-  end
-
   @doc """
   Returns an `%Ecto.Changeset{}` for changing the <%= schema.singular %> password.
 
+  See `<%= inspect context.module %>.<%= inspect schema.alias %>.password_changeset/3` for a list of supported options.
+
   ## Examples
 
       iex> change_<%= schema.singular %>_password(<%= schema.singular %>)
       %Ecto.Changeset{data: %<%= inspect schema.alias %>{}}
 
   """
-  def change_<%= schema.singular %>_password(<%= schema.singular %>, attrs \\ %{}) do
-    <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs, hash_password: false)
+  def change_<%= schema.singular %>_password(<%= schema.singular %>, attrs \\ %{}, opts \\ []) do
+    <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs, opts)
   end
 
   @doc """
   Updates the <%= schema.singular %> password.
 
+  Returns the updated <%= schema.singular %>, as well as a list of expired tokens.
+
   ## Examples
 
-      iex> update_<%= schema.singular %>_password(<%= schema.singular %>, "valid password", %{password: ...})
-      {:ok, %<%= inspect schema.alias %>{}}
+      iex> update_<%= schema.singular %>_password(<%= schema.singular %>, %{password: ...})
+      {:ok, %<%= inspect schema.alias %>{}, [...]}
 
-      iex> update_<%= schema.singular %>_password(<%= schema.singular %>, "invalid password", %{password: ...})
+      iex> update_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "too short"})
       {:error, %Ecto.Changeset{}}
 
   """
-  def update_<%= schema.singular %>_password(<%= schema.singular %>, password, attrs) do
-    changeset =
-      <%= schema.singular %>
-      |> <%= inspect schema.alias %>.password_changeset(attrs)
-      |> <%= inspect schema.alias %>.validate_current_password(password)
-
-    Ecto.Multi.new()
-    |> Ecto.Multi.update(:<%= schema.singular %>, changeset)
-    |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
-    |> Repo.transaction()
+  def update_<%= schema.singular %>_password(<%= schema.singular %>, attrs) do
+    <%= schema.singular %>
+    |> <%= inspect schema.alias %>.password_changeset(attrs)
+    |> update_<%= schema.singular %>_and_delete_all_tokens()
     |> case do
-      {:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
+      {:ok, <%= schema.singular %>, expired_tokens} -> {:ok, <%= schema.singular %>, expired_tokens}
       {:error, :<%= schema.singular %>, changeset, _} -> {:error, changeset}
     end
   end
@@ -227,118 +187,111 @@
   end
 
   @doc """
-  Deletes the signed token with the given context.
-  """
-  def delete_<%= schema.singular %>_session_token(token) do
-    Repo.delete_all(<%= inspect schema.alias %>Token.by_token_and_context_query(token, "session"))
-    :ok
-  end
-
-  ## Confirmation
-
-  @doc ~S"""
-  Delivers the confirmation email instructions to the given <%= schema.singular %>.
-
-  ## Examples
-
-      iex> deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/confirm/#{&1}"))
-      {:ok, %{to: ..., body: ...}}
-
-      iex> deliver_<%= schema.singular %>_confirmation_instructions(confirmed_<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/confirm/#{&1}"))
-      {:error, :already_confirmed}
-
+  Gets the <%= schema.singular %> with the given magic link token.
   """
-  def deliver_<%= schema.singular %>_confirmation_instructions(%<%= inspect schema.alias %>{} = <%= schema.singular %>, confirmation_url_fun)
-      when is_function(confirmation_url_fun, 1) do
-    if <%= schema.singular %>.confirmed_at do
-      {:error, :already_confirmed}
+  def get_<%= schema.singular %>_by_magic_link_token(token) do
+    with {:ok, query} <- <%= inspect schema.alias %>Token.verify_magic_link_token_query(token),
+         {<%= schema.singular %>, _token} <- Repo.one(query) do
+      <%= schema.singular %>
     else
-      {encoded_token, <%= schema.singular %>_token} = <%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "confirm")
-      Repo.insert!(<%= schema.singular %>_token)
-      <%= inspect schema.alias %>Notifier.deliver_confirmation_instructions(<%= schema.singular %>, confirmation_url_fun.(encoded_token))
+      _ -> nil
     end
   end
 
   @doc """
-  Confirms a <%= schema.singular %> by the given token.
+  Logs the <%= schema.singular %> in by magic link.
+
+  There are three cases to consider:
+
+  1. The <%= schema.singular %> has already confirmed their email. They are logged in
+     and the magic link is expired.
+
+  2. The <%= schema.singular %> has not confirmed their email and no password is set.
+     In this case, the <%= schema.singular %> gets confirmed, logged in, and all tokens -
+     including session ones - are expired. In theory, no other tokens
+     exist but we delete all of them for best security practices.
 
-  If the token matches, the <%= schema.singular %> account is marked as confirmed
-  and the token is deleted.
+  3. The <%= schema.singular %> has not confirmed their email but a password is set.
+     This cannot happen in the default implementation but may be the
+     source of security pitfalls. See the "Mixing magic link and password registration" section of
+     `mix help phx.gen.auth`.
   """
-  def confirm_<%= schema.singular %>(token) do
-    with {:ok, query} <- <%= inspect schema.alias %>Token.verify_email_token_query(token, "confirm"),
-         %<%= inspect schema.alias %>{} = <%= schema.singular %> <- Repo.one(query),
-         {:ok, %{<%= schema.singular %>: <%= schema.singular %>}} <- Repo.transaction(confirm_<%= schema.singular %>_multi(<%= schema.singular %>)) do
-      {:ok, <%= schema.singular %>}
-    else
-      _ -> :error
+  def login_<%= schema.singular %>_by_magic_link(token) do
+    {:ok, query} = <%= inspect schema.alias %>Token.verify_magic_link_token_query(token)
+
+    case Repo.one(query) do
+      # Prevent session fixation attacks by disallowing magic links for unconfirmed users with password
+      {%<%= inspect schema.alias %>{confirmed_at: nil, hashed_password: hash}, _token} when not is_nil(hash) ->
+        raise """
+        magic link log in is not allowed for unconfirmed users with a password set!
+
+        This cannot happen with the default implementation, which indicates that you
+        might have adapted the code to a different use case. Please make sure to read the
+        "Mixing magic link and password registration" section of `mix help phx.gen.auth`.
+        """
+
+      {%<%= inspect schema.alias %>{confirmed_at: nil} = <%= schema.singular %>, _token} ->
+        <%= schema.singular %>
+        |> <%= inspect schema.alias %>.confirm_changeset()
+        |> update_<%= schema.singular %>_and_delete_all_tokens()
+
+      {<%= schema.singular %>, token} ->
+        Repo.delete!(token)
+        {:ok, <%= schema.singular %>, []}
+
+      nil ->
+        {:error, :not_found}
     end
   end
 
-  defp confirm_<%= schema.singular %>_multi(<%= schema.singular %>) do
-    Ecto.Multi.new()
-    |> Ecto.Multi.update(:<%= schema.singular %>, <%= inspect schema.alias %>.confirm_changeset(<%= schema.singular %>))
-    |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, ["confirm"]))
-  end
-
-  ## Reset password
-
   @doc ~S"""
-  Delivers the reset password email to the given <%= schema.singular %>.
+  Delivers the update email instructions to the given <%= schema.singular %>.
 
   ## Examples
 
-      iex> deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}"))
+      iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}"))
       {:ok, %{to: ..., body: ...}}
 
   """
-  def deliver_<%= schema.singular %>_reset_password_instructions(%<%= inspect schema.alias %>{} = <%= schema.singular %>, reset_password_url_fun)
-      when is_function(reset_password_url_fun, 1) do
-    {encoded_token, <%= schema.singular %>_token} = <%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "reset_password")
+  def deliver_<%= schema.singular %>_update_email_instructions(%<%= inspect schema.alias %>{} = <%= schema.singular %>, current_email, update_email_url_fun)
+      when is_function(update_email_url_fun, 1) do
+    {encoded_token, <%= schema.singular %>_token} = <%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "change:#{current_email}")
+
     Repo.insert!(<%= schema.singular %>_token)
-    <%= inspect schema.alias %>Notifier.deliver_reset_password_instructions(<%= schema.singular %>, reset_password_url_fun.(encoded_token))
+    <%= inspect schema.alias %>Notifier.deliver_update_email_instructions(<%= schema.singular %>, update_email_url_fun.(encoded_token))
   end
 
-  @doc """
-  Gets the <%= schema.singular %> by reset password token.
-
-  ## Examples
-
-      iex> get_<%= schema.singular %>_by_reset_password_token("validtoken")
-      %<%= inspect schema.alias %>{}
-
-      iex> get_<%= schema.singular %>_by_reset_password_token("invalidtoken")
-      nil
-
+  @doc ~S"""
+  Delivers the magic link login instructions to the given <%= schema.singular %>.
   """
-  def get_<%= schema.singular %>_by_reset_password_token(token) do
-    with {:ok, query} <- <%= inspect schema.alias %>Token.verify_email_token_query(token, "reset_password"),
-         %<%= inspect schema.alias %>{} = <%= schema.singular %> <- Repo.one(query) do
-      <%= schema.singular %>
-    else
-      _ -> nil
-    end
+  def deliver_login_instructions(%<%= inspect schema.alias %>{} = <%= schema.singular %>, magic_link_url_fun)
+      when is_function(magic_link_url_fun, 1) do
+    {encoded_token, <%= schema.singular %>_token} = <%= inspect schema.alias %>Token.build_email_token(<%= schema.singular %>, "login")
+    Repo.insert!(<%= schema.singular %>_token)
+    <%= inspect schema.alias %>Notifier.deliver_login_instructions(<%= schema.singular %>, magic_link_url_fun.(encoded_token))
   end
 
   @doc """
-  Resets the <%= schema.singular %> password.
-
-  ## Examples
+  Deletes the signed token with the given context.
+  """
+  def delete_<%= schema.singular %>_session_token(token) do
+    Repo.delete_all(<%= inspect schema.alias %>Token.by_token_and_context_query(token, "session"))
+    :ok
+  end
 
-      iex> reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "new long password", password_confirmation: "new long password"})
-      {:ok, %<%= inspect schema.alias %>{}}
+  ## Token helper
 
-      iex> reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "valid", password_confirmation: "not the same"})
-      {:error, %Ecto.Changeset{}}
+  defp update_<%= schema.singular %>_and_delete_all_tokens(changeset) do
+    %{data: %<%= inspect schema.alias %>{} = <%= schema.singular %>} = changeset
 
-  """
-  def reset_<%= schema.singular %>_password(<%= schema.singular %>, attrs) do
-    Ecto.Multi.new()
-    |> Ecto.Multi.update(:<%= schema.singular %>, <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs))
-    |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
-    |> Repo.transaction()
-    |> case do
-      {:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
-      {:error, :<%= schema.singular %>, changeset, _} -> {:error, changeset}
+    with {:ok, %{<%= schema.singular %>: <%= schema.singular %>, tokens_to_expire: expired_tokens}} <-
+           Ecto.Multi.new()
+           |> Ecto.Multi.update(:<%= schema.singular %>, changeset)
+           |> Ecto.Multi.all(:tokens_to_expire, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
+           |> Ecto.Multi.delete_all(:tokens, fn %{tokens_to_expire: tokens_to_expire} ->
+             <%= inspect schema.alias %>Token.delete_all_query(tokens_to_expire)
+           end)
+           |> Repo.transaction() do
+      {:ok, <%= schema.singular %>, expired_tokens}
     end
   end
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
deleted file mode 100644
index 9c30c3c90..000000000
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ /dev/null
@@ -1,56 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPassword do
-  use <%= inspect context.web_module %>, :live_view
-
-  alias <%= inspect context.module %>
-
-  def render(assigns) do
-    ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">
-        Forgot your password?
-        <:subtitle>We'll send a password reset link to your inbox</:subtitle>
-      </.header>
-
-      <.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
-        <.input
-          field={@form[:email]}
-          type="email"
-          placeholder="Email"
-          autocomplete="username"
-          required
-        />
-        <:actions>
-          <.button phx-disable-with="Sending..." class="w-full">
-            Send password reset instructions
-          </.button>
-        </:actions>
-      </.simple_form>
-      <p class="text-center text-sm mt-4">
-        <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-      </p>
-    </div>
-    """
-  end
-
-  def mount(_params, _session, socket) do
-    {:ok, assign(socket, form: to_form(%{}, as: "<%= schema.singular %>"))}
-  end
-
-  def handle_event("send_email", %{"<%= schema.singular %>" => %{"email" => email}}, socket) do
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
-      <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
-        <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
-      )
-    end
-
-    info =
-      "If your email is in our system, you will receive instructions to reset your password shortly."
-
-    {:noreply,
-     socket
-     |> put_flash(:info, info)
-     |> redirect(to: ~p"/")}
-  end
-end
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 77bb16d79..8924831e9 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -1,49 +1,127 @@
 defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Login do
   use <%= inspect context.web_module %>, :live_view
 
+  alias <%= inspect context.module %>
+
   def render(assigns) do
     ~H"""
     <div class="mx-auto max-w-sm">
       <.header class="text-center">
-        Log in to account
+        <p>Log in</p>
         <:subtitle>
-          Don't have an account?
-          <.link navigate={~p"<%= schema.route_prefix %>/register"} class="font-semibold text-brand hover:underline">
-            Sign up
-          </.link>
-          for an account now.
+          <%%= if @current_<%= schema.singular %> do %>
+            You need to reauthenticate to perform sensitive actions on your account.
+          <%% else %>
+            Don't have an account? <.link
+              navigate={~p"<%= schema.route_prefix %>/register"}
+              class="font-semibold text-brand hover:underline"
+              phx-no-format
+            >Sign up</.link> for an account now.
+          <%% end %>
         </:subtitle>
       </.header>
 
-      <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"} phx-update="ignore">
-        <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
+      <.simple_form
+        :let={f}
+        for={@form}
+        id="login_form_magic"
+        action={~p"<%= schema.route_prefix %>/log-in"}
+        phx-submit="submit_magic"
+      >
+        <.input
+          readonly={!!@current_<%= schema.singular %>}
+          field={f[:email]}
+          type="email"
+          label="Email"
+          autocomplete="username"
+          required
+        />
+        <.button class="w-full">
+          Log in with email <span aria-hidden="true">→</span>
+        </.button>
+      </.simple_form>
+
+      <div class="flex items-center mt-8 -mb-6 text-sm">
+        <hr class="flex-grow border-t-2 border-gray-300" />
+        <span class="mx-2 text-gray-500">or</span>
+        <hr class="flex-grow border-t-2 border-gray-300" />
+      </div>
+
+      <.simple_form
+        :let={f}
+        for={@form}
+        id="login_form_password"
+        action={~p"<%= schema.route_prefix %>/log-in"}
+        phx-submit="submit_password"
+        phx-trigger-action={@trigger_submit}
+      >
+        <.input
+          readonly={!!@current_<%= schema.singular %>}
+          field={f[:email]}
+          type="email"
+          label="Email"
+          autocomplete="username"
+          required
+        />
         <.input
           field={@form[:password]}
           type="password"
           label="Password"
           autocomplete="current-password"
-          required
         />
-
-        <:actions>
-          <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
-          <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
-            Forgot your password?
-          </.link>
-        </:actions>
-        <:actions>
-          <.button class="w-full">
-            Log in <span aria-hidden="true">→</span>
-          </.button>
-        </:actions>
+        <.input
+          :if={!@current_<%= schema.singular %>}
+          field={f[:remember_me]}
+          type="checkbox"
+          label="Keep me logged in"
+        />
+        <.button class="w-full">
+          Log in <span aria-hidden="true">→</span>
+        </.button>
       </.simple_form>
+
+      <div :if={local_mail_adapter?()} class="mt-8 p-4 border text-center">
+        <p>You are running the local mail adapter.</p>
+        <p>
+          To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+        </p>
+      </div>
     </div>
     """
   end
 
   def mount(_params, _session, socket) do
-    email = Phoenix.Flash.get(socket.assigns.flash, :email)
+    email =
+      Phoenix.Flash.get(socket.assigns.flash, :email) ||
+        get_in(socket.assigns, [:current_<%= schema.singular %>, Access.key(:email)])
+
     form = to_form(%{"email" => email}, as: "<%= schema.singular %>")
-    {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
+
+    {:ok, assign(socket, form: form, trigger_submit: false)}
+  end
+
+  def handle_event("submit_password", _params, socket) do
+    {:noreply, assign(socket, :trigger_submit, true)}
+  end
+
+  def handle_event("submit_magic", %{"<%= schema.singular %>" => %{"email" => email}}, socket) do
+    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
+      <%= inspect context.alias %>.deliver_login_instructions(
+        <%= schema.singular %>,
+        &url(~p"<%= schema.route_prefix %>/log-in/#{&1}")
+      )
+    end
+
+    info =
+      "If your email is in our system, you will receive instructions for logging in shortly."
+
+    {:noreply,
+     socket
+     |> put_flash(:info, info)
+     |> push_navigate(to: ~p"<%= schema.route_prefix %>/log-in")}
+  end
+
+  defp local_mail_adapter? do
+    Application.get_env(:<%= Mix.Phoenix.otp_app() %>, <%= inspect context.base_module %>.Mailer)[:adapter] == Swoosh.Adapters.Local
   end
 end
diff --git a/priv/templates/phx.gen.auth/migration.ex b/priv/templates/phx.gen.auth/migration.ex
index ab8d4b918..4219a23cd 100644
--- a/priv/templates/phx.gen.auth/migration.ex
+++ b/priv/templates/phx.gen.auth/migration.ex
@@ -7,7 +7,7 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
     create table(:<%= schema.table %><%= if schema.binary_id do %>, primary_key: false<% end %>) do
 <%= if schema.binary_id do %>      add :id, :binary_id, primary_key: true
 <% end %>      <%= migration.column_definitions[:email] %>
-      add :hashed_password, :string, null: false
+      add :hashed_password, :string
       add :confirmed_at, <%= inspect schema.timestamp_type %>
 
       timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
diff --git a/priv/templates/phx.gen.auth/notifier.ex b/priv/templates/phx.gen.auth/notifier.ex
index a8a0a58a4..a74a96f26 100644
--- a/priv/templates/phx.gen.auth/notifier.ex
+++ b/priv/templates/phx.gen.auth/notifier.ex
@@ -2,6 +2,7 @@ defmodule <%= inspect context.module %>.<%= inspect schema.alias %>Notifier do
   import Swoosh.Email
 
   alias <%= inspect context.base_module %>.Mailer
+  alias <%= inspect context.module %>.<%= inspect schema.alias %>
 
   # Delivers the email using the application mailer.
   defp deliver(recipient, subject, body) do
@@ -18,60 +19,64 @@ defmodule <%= inspect context.module %>.<%= inspect schema.alias %>Notifier do
   end
 
   @doc """
-  Deliver instructions to confirm account.
+  Deliver instructions to update a <%= schema.singular %> email.
   """
-  def deliver_confirmation_instructions(<%= schema.singular %>, url) do
-    deliver(<%= schema.singular %>.email, "Confirmation instructions", """
+  def deliver_update_email_instructions(<%= schema.singular %>, url) do
+    deliver(<%= schema.singular %>.email, "Update email instructions", """
 
     ==============================
 
     Hi #{<%= schema.singular %>.email},
 
-    You can confirm your account by visiting the URL below:
+    You can change your email by visiting the URL below:
 
     #{url}
 
-    If you didn't create an account with us, please ignore this.
+    If you didn't request this change, please ignore this.
 
     ==============================
     """)
   end
 
   @doc """
-  Deliver instructions to reset a <%= schema.singular %> password.
+  Deliver instructions to log in with a magic link.
   """
-  def deliver_reset_password_instructions(<%= schema.singular %>, url) do
-    deliver(<%= schema.singular %>.email, "Reset password instructions", """
+  def deliver_login_instructions(<%= schema.singular %>, url) do
+    case <%= schema.singular %> do
+      %<%= inspect schema.alias %>{confirmed_at: nil} -> deliver_confirmation_instructions(<%= schema.singular %>, url)
+      _ -> deliver_magic_link_instructions(<%= schema.singular %>, url)
+    end
+  end
+
+  defp deliver_magic_link_instructions(<%= schema.singular %>, url) do
+    deliver(<%= schema.singular %>.email, "Log in instructions", """
 
     ==============================
 
     Hi #{<%= schema.singular %>.email},
 
-    You can reset your password by visiting the URL below:
+    You can log into your account by visiting the URL below:
 
     #{url}
 
-    If you didn't request this change, please ignore this.
+    If you didn't request this email, please ignore this.
 
     ==============================
     """)
   end
 
-  @doc """
-  Deliver instructions to update a <%= schema.singular %> email.
-  """
-  def deliver_update_email_instructions(<%= schema.singular %>, url) do
-    deliver(<%= schema.singular %>.email, "Update email instructions", """
+  defp deliver_confirmation_instructions(<%= schema.singular %>, url) do
+    deliver(<%= schema.singular %>.email, "Confirmation instructions", """
 
     ==============================
 
     Hi #{<%= schema.singular %>.email},
 
-    You can change your email by visiting the URL below:
+    You can confirm your account by visiting the URL below:
 
     #{url}
 
-    If you didn't request this change, please ignore this.
+    If you didn't create an account with us, please ignore this.
 
     ==============================
     """)
diff --git a/priv/templates/phx.gen.auth/registration_controller.ex b/priv/templates/phx.gen.auth/registration_controller.ex
index 46752454d..eb361e40d 100644
--- a/priv/templates/phx.gen.auth/registration_controller.ex
+++ b/priv/templates/phx.gen.auth/registration_controller.ex
@@ -3,10 +3,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   alias <%= inspect context.module %>
   alias <%= inspect schema.module %>
-  alias <%= inspect auth_module %>
 
   def new(conn, _params) do
-    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{})
+    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(%<%= inspect schema.alias %>{})
     render(conn, :new, changeset: changeset)
   end
 
@@ -14,14 +13,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     case <%= inspect context.alias %>.register_<%= schema.singular %>(<%= schema.singular %>_params) do
       {:ok, <%= schema.singular %>} ->
         {:ok, _} =
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(
+          <%= inspect context.alias %>.deliver_login_instructions(
             <%= schema.singular %>,
-            &url(~p"<%= schema.route_prefix %>/confirm/#{&1}")
+            &url(~p"<%= schema.route_prefix %>/log-in/#{&1}")
           )
 
         conn
-        |> put_flash(:info, "<%= schema.human_singular %> created successfully.")
-        |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>)
+        |> put_flash(
+          :info,
+          "An email was sent to #{<%= schema.singular %>.email}, please access it to confirm your account."
+        )
+        |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
 
       {:error, %Ecto.Changeset{} = changeset} ->
         render(conn, :new, changeset: changeset)
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index 31cb8ad8e..6675ec625 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -18,27 +18,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         </:subtitle>
       </.header>
 
-      <.simple_form
-        for={@form}
-        id="registration_form"
-        phx-submit="save"
-        phx-change="validate"
-        phx-trigger-action={@trigger_submit}
-        action={~p"<%= schema.route_prefix %>/log-in?_action=registered"}
-        method="post"
-      >
+      <.simple_form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
         <.error :if={@check_errors}>
           Oops, something went wrong! Please check the errors below.
         </.error>
 
         <.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
-        <.input
-          field={@form[:password]}
-          type="password"
-          label="Password"
-          autocomplete="new-password"
-          required
-        />
 
         <:actions>
           <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
@@ -48,12 +33,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     """
   end
 
+  def mount(_params, _session, %{assigns: %{current_<%= schema.singular %>: <%= schema.singular %>}} = socket)
+      when not is_nil(<%= schema.singular %>) do
+    {:ok, redirect(socket, to: <%= inspect auth_module %>.signed_in_path(socket))}
+  end
+
   def mount(_params, _session, socket) do
-    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{})
+    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(%<%= inspect schema.alias %>{})
 
     socket =
       socket
-      |> assign(trigger_submit: false, check_errors: false)
+      |> assign(check_errors: false)
       |> assign_form(changeset)
 
     {:ok, socket, temporary_assigns: [form: nil]}
@@ -63,13 +53,18 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     case <%= inspect context.alias %>.register_<%= schema.singular %>(<%= schema.singular %>_params) do
       {:ok, <%= schema.singular %>} ->
         {:ok, _} =
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(
+          <%= inspect context.alias %>.deliver_login_instructions(
             <%= schema.singular %>,
-            &url(~p"<%= schema.route_prefix %>/confirm/#{&1}")
+            &url(~p"<%= schema.route_prefix %>/log-in/#{&1}")
           )
 
-        changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(<%= schema.singular %>)
-        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
+        {:noreply,
+         socket
+         |> put_flash(
+           :info,
+           "An email was sent to #{<%= schema.singular %>.email}, please access it to confirm your account."
+         )
+         |> push_navigate(to: ~p"<%= schema.route_prefix %>/log-in")}
 
       {:error, %Ecto.Changeset{} = changeset} ->
         {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
@@ -77,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
-    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{}, <%= schema.singular %>_params)
+    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(%<%= inspect schema.alias %>{}, <%= schema.singular %>_params)
     {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
   end
 
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index cff2b3819..640e67ef2 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -16,13 +16,6 @@
     </.error>
 
     <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
-    <.input
-      field={f[:password]}
-      type="password"
-      label="Password"
-      autocomplete="new-password"
-      required
-    />
 
     <:actions>
       <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
diff --git a/priv/templates/phx.gen.auth/reset_password_controller.ex b/priv/templates/phx.gen.auth/reset_password_controller.ex
deleted file mode 100644
index e572e4d5f..000000000
--- a/priv/templates/phx.gen.auth/reset_password_controller.ex
+++ /dev/null
@@ -1,58 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordController do
-  use <%= inspect context.web_module %>, :controller
-
-  alias <%= inspect context.module %>
-
-  plug :get_<%= schema.singular %>_by_reset_password_token when action in [:edit, :update]
-
-  def new(conn, _params) do
-    render(conn, :new)
-  end
-
-  def create(conn, %{"<%= schema.singular %>" => %{"email" => email}}) do
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
-      <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
-        <%= schema.singular %>,
-        &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
-      )
-    end
-
-    conn
-    |> put_flash(
-      :info,
-      "If your email is in our system, you will receive instructions to reset your password shortly."
-    )
-    |> redirect(to: ~p"/")
-  end
-
-  def edit(conn, _params) do
-    render(conn, :edit, changeset: <%= inspect context.alias %>.change_<%= schema.singular %>_password(conn.assigns.<%= schema.singular %>))
-  end
-
-  # Do not log in the <%= schema.singular %> after reset password to avoid a
-  # leaked token giving the <%= schema.singular %> access to the account.
-  def update(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params}) do
-    case <%= inspect context.alias %>.reset_<%= schema.singular %>_password(conn.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
-      {:ok, _} ->
-        conn
-        |> put_flash(:info, "Password reset successfully.")
-        |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
-
-      {:error, changeset} ->
-        render(conn, :edit, changeset: changeset)
-    end
-  end
-
-  defp get_<%= schema.singular %>_by_reset_password_token(conn, _opts) do
-    %{"token" => token} = conn.params
-
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_reset_password_token(token) do
-      conn |> assign(:<%= schema.singular %>, <%= schema.singular %>) |> assign(:token, token)
-    else
-      conn
-      |> put_flash(:error, "Reset password link is invalid or it has expired.")
-      |> redirect(to: ~p"/")
-      |> halt()
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/reset_password_edit.html.heex b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
deleted file mode 100644
index 729b497c4..000000000
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">
-    Reset Password
-  </.header>
-
-  <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset-password/#{@token}"}>
-    <.error :if={@changeset.action}>
-      Oops, something went wrong! Please check the errors below.
-    </.error>
-
-    <.input
-      field={f[:password]}
-      type="password"
-      label="New Password"
-      autocomplete="new-password"
-      required
-    />
-    <.input
-      field={f[:password_confirmation]}
-      type="password"
-      label="Confirm new password"
-      autocomplete="new-password"
-      required
-    />
-    <:actions>
-      <.button phx-disable-with="Resetting..." class="w-full">
-        Reset password
-      </.button>
-    </:actions>
-  </.simple_form>
-
-  <p class="text-center text-sm mt-4">
-    <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-  </p>
-</div>
diff --git a/priv/templates/phx.gen.auth/reset_password_html.ex b/priv/templates/phx.gen.auth/reset_password_html.ex
deleted file mode 100644
index 332b9356d..000000000
--- a/priv/templates/phx.gen.auth/reset_password_html.ex
+++ /dev/null
@@ -1,5 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordHTML do
-  use <%= inspect context.web_module %>, :html
-
-  embed_templates "<%= schema.singular %>_reset_password_html/*"
-end
diff --git a/priv/templates/phx.gen.auth/reset_password_live.ex b/priv/templates/phx.gen.auth/reset_password_live.ex
deleted file mode 100644
index 02ae26850..000000000
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ /dev/null
@@ -1,96 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPassword do
-  use <%= inspect context.web_module %>, :live_view
-
-  alias <%= inspect context.module %>
-
-  def render(assigns) do
-    ~H"""
-    <div class="mx-auto max-w-sm">
-      <.header class="text-center">Reset Password</.header>
-
-      <.simple_form
-        for={@form}
-        id="reset_password_form"
-        phx-submit="reset_password"
-        phx-change="validate"
-      >
-        <.error :if={@form.errors != []}>
-          Oops, something went wrong! Please check the errors below.
-        </.error>
-
-        <.input
-          field={@form[:password]}
-          type="password"
-          label="New password"
-          autocomplete="new-password"
-          required
-        />
-        <.input
-          field={@form[:password_confirmation]}
-          type="password"
-          label="Confirm new password"
-          autocomplete="new-password"
-          required
-        />
-        <:actions>
-          <.button phx-disable-with="Resetting..." class="w-full">Reset Password</.button>
-        </:actions>
-      </.simple_form>
-
-      <p class="text-center text-sm mt-4">
-        <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-        | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-      </p>
-    </div>
-    """
-  end
-
-  def mount(params, _session, socket) do
-    socket = assign_<%= schema.singular %>_and_token(socket, params)
-
-    form_source =
-      case socket.assigns do
-        %{<%= schema.singular %>: <%= schema.singular %>} ->
-          <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>)
-
-        _ ->
-          %{}
-      end
-
-    {:ok, assign_form(socket, form_source), temporary_assigns: [form: nil]}
-  end
-
-  # Do not log in the <%= schema.singular %> after reset password to avoid a
-  # leaked token giving the <%= schema.singular %> access to the account.
-  def handle_event("reset_password", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
-    case <%= inspect context.alias %>.reset_<%= schema.singular %>_password(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
-      {:ok, _} ->
-        {:noreply,
-         socket
-         |> put_flash(:info, "Password reset successfully.")
-         |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")}
-
-      {:error, changeset} ->
-        {:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
-    end
-  end
-
-  def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
-    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params)
-    {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
-  end
-
-  defp assign_<%= schema.singular %>_and_token(socket, %{"token" => token}) do
-    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_reset_password_token(token) do
-      assign(socket, <%= schema.singular %>: <%= schema.singular %>, token: token)
-    else
-      socket
-      |> put_flash(:error, "Reset password link is invalid or it has expired.")
-      |> redirect(to: ~p"/")
-    end
-  end
-
-  defp assign_form(socket, %{} = source) do
-    assign(socket, :form, to_form(source, as: "<%= schema.singular %>"))
-  end
-end
diff --git a/priv/templates/phx.gen.auth/reset_password_new.html.heex b/priv/templates/phx.gen.auth/reset_password_new.html.heex
deleted file mode 100644
index 6f254d1e8..000000000
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ /dev/null
@@ -1,20 +0,0 @@
-<div class="mx-auto max-w-sm">
-  <.header class="text-center">
-    Forgot your password?
-    <:subtitle>We'll send a password reset link to your inbox</:subtitle>
-  </.header>
-
-  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset-password"}>
-    <.input field={f[:email]} type="email" placeholder="Email" autocomplete="username" required />
-    <:actions>
-      <.button phx-disable-with="Sending..." class="w-full">
-        Send password reset instructions
-      </.button>
-    </:actions>
-  </.simple_form>
-
-  <p class="text-center text-sm mt-4">
-    <.link href={~p"<%= schema.route_prefix %>/register"}>Register</.link>
-    | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in</.link>
-  </p>
-</div>
diff --git a/priv/templates/phx.gen.auth/routes.ex b/priv/templates/phx.gen.auth/routes.ex
index 928ea70c4..b68abf4c5 100644
--- a/priv/templates/phx.gen.auth/routes.ex
+++ b/priv/templates/phx.gen.auth/routes.ex
@@ -1,37 +1,23 @@
 
   ## Authentication routes
 
-  scope <%= router_scope %> do
-    pipe_through [:browser, :redirect_if_<%= schema.singular %>_is_authenticated]<%= if live? do %>
-
-    live_session :redirect_if_<%= schema.singular %>_is_authenticated,
-      on_mount: [{<%= inspect auth_module %>, :redirect_if_<%= schema.singular %>_is_authenticated}] do
-      live "/<%= schema.plural %>/register", <%= inspect schema.alias %>Live.Registration, :new
-      live "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>Live.Login, :new
-      live "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>Live.ForgotPassword, :new
-      live "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>Live.ResetPassword, :edit
-    end
-
-    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create<% else %>
+  <%= if not live? do %>scope <%= router_scope %> do
+    pipe_through [:browser, :redirect_if_<%= schema.singular %>_is_authenticated]
 
     get "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :new
     post "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :create
-    get "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :new
-    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create
-    get "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :new
-    post "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :create
-    get "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :edit
-    put "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :update<% end %>
   end
 
-  scope <%= router_scope %> do
+  <% end %>scope <%= router_scope %> do
     pipe_through [:browser, :require_authenticated_<%= schema.singular %>]<%= if live? do %>
 
     live_session :require_authenticated_<%= schema.singular %>,
       on_mount: [{<%= inspect auth_module %>, :ensure_authenticated}] do
       live "/<%= schema.plural %>/settings", <%= inspect schema.alias %>Live.Settings, :edit
       live "/<%= schema.plural %>/settings/confirm-email/:token", <%= inspect schema.alias %>Live.Settings, :confirm_email
-    end<% else %>
+    end
+
+    post "/<%= schema.plural %>/update-password", <%= inspect schema.alias %>SessionController, :update_password<% else %>
 
     get "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :edit
     put "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :update
@@ -41,15 +27,16 @@
   scope <%= router_scope %> do
     pipe_through [:browser]
 
-    delete "/<%= schema.plural %>/log-out", <%= inspect schema.alias %>SessionController, :delete<%= if live? do %>
-
-    live_session :current_<%= schema.singular %>,
+    <%= if live? do %>live_session :current_<%= schema.singular %>,
       on_mount: [{<%= inspect auth_module %>, :mount_current_<%= schema.singular %>}] do
-      live "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>Live.Confirmation, :edit
-      live "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>Live.ConfirmationInstructions, :new
-    end<% else %>
-    get "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :new
-    post "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :create
-    get "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>ConfirmationController, :edit
-    post "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>ConfirmationController, :update<% end %>
+      live "/<%= schema.plural %>/register", <%= inspect schema.alias %>Live.Registration, :new
+      live "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>Live.Login, :new
+      live "/<%= schema.plural %>/log-in/:token", <%= inspect schema.alias %>Live.Confirmation, :new
+    end
+
+    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create
+    delete "/<%= schema.plural %>/log-out", <%= inspect schema.alias %>SessionController, :delete<% else %>get "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :new
+    get "/<%= schema.plural %>/log-in/:token", <%= inspect schema.alias %>SessionController, :confirm
+    post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create
+    delete "/<%= schema.plural %>/log-out", <%= inspect schema.alias %>SessionController, :delete<% end %>
   end
diff --git a/priv/templates/phx.gen.auth/schema.ex b/priv/templates/phx.gen.auth/schema.ex
index b289b3e0b..dae8f1cbe 100644
--- a/priv/templates/phx.gen.auth/schema.ex
+++ b/priv/templates/phx.gen.auth/schema.ex
@@ -9,106 +9,61 @@ defmodule <%= inspect schema.module %> do
     field :hashed_password, :string, redact: true
     field :current_password, :string, virtual: true, redact: true
     field :confirmed_at, <%= inspect schema.timestamp_type %>
+    field :authenticated_at, <%= inspect schema.timestamp_type %>, virtual: true
 
     timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
   end
 
   @doc """
-  A <%= schema.singular %> changeset for registration.
+  A <%= schema.singular %> changeset for registering or changing the email.
 
-  It is important to validate the length of both email and password.
-  Otherwise databases may truncate the email without warnings, which
-  could lead to unpredictable or insecure behaviour. Long passwords may
-  also be very expensive to hash for certain algorithms.
+  It requires the email to change otherwise an error is added.
 
   ## Options
 
-    * `:hash_password` - Hashes the password so it can be stored securely
-      in the database and ensures the password field is cleared to prevent
-      leaks in the logs. If password hashing is not needed and clearing the
-      password field is not desired (like when using this changeset for
-      validations on a LiveView form), this option can be set to `false`.
-      Defaults to `true`.
-
-    * `:validate_email` - Validates the uniqueness of the email, in case
-      you don't want to validate the uniqueness of the email (like when
-      using this changeset for validations on a LiveView form before
-      submitting the form), this option can be set to `false`.
+    * `:validate_email` - Set to false if you don't want to validate the
+      uniqueness of the email, useful when displaying live validations.
       Defaults to `true`.
   """
-  def registration_changeset(<%= schema.singular %>, attrs, opts \\ []) do
+  def email_changeset(<%= schema.singular %>, attrs, opts \\ []) do
     <%= schema.singular %>
-    |> cast(attrs, [:email, :password])
+    |> cast(attrs, [:email])
     |> validate_email(opts)
-    |> validate_password(opts)
   end
 
   defp validate_email(changeset, opts) do
-    changeset
-    |> validate_required([:email])
-    |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/,
-      message: "must have the @ sign and no spaces"
-    )
-    |> validate_length(:email, max: 160)
-    |> maybe_validate_unique_email(opts)
-  end
-
-  defp validate_password(changeset, opts) do
-    changeset
-    |> validate_required([:password])
-    |> validate_length(:password, min: 12, max: 72)
-    # Examples of additional password validation:
-    # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
-    # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
-    # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
-    |> maybe_hash_password(opts)
-  end
-
-  defp maybe_hash_password(changeset, opts) do
-    hash_password? = Keyword.get(opts, :hash_password, true)
-    password = get_change(changeset, :password)
-
-    if hash_password? && password && changeset.valid? do
-      changeset<%= if hashing_library.name == :bcrypt do %>
-      # If using Bcrypt, then further validate it is at most 72 bytes long
-      |> validate_length(:password, max: 72, count: :bytes)<% end %>
-      # Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
-      # would keep the database transaction open longer and hurt performance.
-      |> put_change(:hashed_password, <%= inspect hashing_library.module %>.hash_pwd_salt(password))
-      |> delete_change(:password)
-    else
+    changeset =
       changeset
-    end
-  end
+      |> validate_required([:email])
+      |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/,
+        message: "must have the @ sign and no spaces"
+      )
+      |> validate_length(:email, max: 160)
 
-  defp maybe_validate_unique_email(changeset, opts) do
     if Keyword.get(opts, :validate_email, true) do
       changeset
       |> unsafe_validate_unique(:email, <%= inspect schema.repo %>)
       |> unique_constraint(:email)
+      |> validate_email_changed()
     else
       changeset
     end
   end
 
-  @doc """
-  A <%= schema.singular %> changeset for changing the email.
-
-  It requires the email to change otherwise an error is added.
-  """
-  def email_changeset(<%= schema.singular %>, attrs, opts \\ []) do
-    <%= schema.singular %>
-    |> cast(attrs, [:email])
-    |> validate_email(opts)
-    |> case do
-      %{changes: %{email: _}} = changeset -> changeset
-      %{} = changeset -> add_error(changeset, :email, "did not change")
+  defp validate_email_changed(changeset) do
+    if get_field(changeset, :email) && get_change(changeset, :email) == nil do
+      add_error(changeset, :email, "did not change")
+    else
+      changeset
     end
   end
 
   @doc """
   A <%= schema.singular %> changeset for changing the password.
 
+  It is important to validate the length of the password, as long passwords may
+  be very expensive to hash for certain algorithms.
+
   ## Options
 
     * `:hash_password` - Hashes the password so it can be stored securely
@@ -125,6 +80,34 @@ defmodule <%= inspect schema.module %> do
     |> validate_password(opts)
   end
 
+  defp validate_password(changeset, opts) do
+    changeset
+    |> validate_required([:password])
+    |> validate_length(:password, min: 12, max: 72)
+    # Examples of additional password validation:
+    # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
+    # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
+    # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
+    |> maybe_hash_password(opts)
+  end
+
+  defp maybe_hash_password(changeset, opts) do
+    hash_password? = Keyword.get(opts, :hash_password, true)
+    password = get_change(changeset, :password)
+
+    if hash_password? && password && changeset.valid? do
+      changeset<%= if hashing_library.name == :bcrypt do %>
+      # If using Bcrypt, then further validate it is at most 72 bytes long
+      |> validate_length(:password, max: 72, count: :bytes)<% end %>
+      # Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
+      # would keep the database transaction open longer and hurt performance.
+      |> put_change(:hashed_password, <%= inspect hashing_library.module %>.hash_pwd_salt(password))
+      |> delete_change(:password)
+    else
+      changeset
+    end
+  end
+
   @doc """
   Confirms the account by setting `confirmed_at`.
   """
@@ -151,17 +134,4 @@ defmodule <%= inspect schema.module %> do
     <%= inspect hashing_library.module %>.no_user_verify()
     false
   end
-
-  @doc """
-  Validates the current password otherwise adds an error to the changeset.
-  """
-  def validate_current_password(changeset, password) do
-    changeset = cast(changeset, %{current_password: password}, [:current_password])
-
-    if valid_password?(changeset.data, password) do
-      changeset
-    else
-      add_error(changeset, :current_password, "is not valid")
-    end
-  end
 end
diff --git a/priv/templates/phx.gen.auth/schema_token.ex b/priv/templates/phx.gen.auth/schema_token.ex
index d862d1d28..3dcce14f2 100644
--- a/priv/templates/phx.gen.auth/schema_token.ex
+++ b/priv/templates/phx.gen.auth/schema_token.ex
@@ -6,10 +6,9 @@ defmodule <%= inspect schema.module %>Token do
   @hash_algorithm :sha256
   @rand_size 32
 
-  # It is very important to keep the reset password token expiry short,
+  # It is very important to keep the magic link token expiry short,
   # since someone with access to the email may take over the account.
-  @reset_password_validity_in_days 1
-  @confirm_validity_in_days 7
+  @magic_link_validity_in_minutes 15
   @change_email_validity_in_days 7
   @session_validity_in_days 60
 <%= if schema.binary_id do %>
@@ -61,7 +60,8 @@ defmodule <%= inspect schema.module %>Token do
       from token in by_token_and_context_query(token, "session"),
         join: <%= schema.singular %> in assoc(token, :<%= schema.singular %>),
         where: token.inserted_at > ago(@session_validity_in_days, "day"),
-        select: <%= schema.singular %>
+        select: <%= schema.singular %>,
+        select_merge: %{authenticated_at: token.inserted_at}
 
     {:ok, query}
   end
@@ -72,7 +72,7 @@ defmodule <%= inspect schema.module %>Token do
   The non-hashed token is sent to the <%= schema.singular %> email while the
   hashed part is stored in the database. The original token cannot be reconstructed,
   which means anyone with read-only access to the database cannot directly use
-  the token in the application to gain access. Furthermore, if the user changes
+  the token in the application to gain access. Furthermore, if the <%= schema.singular %> changes
   their email in the system, the tokens sent to the previous email are no longer
   valid.
 
@@ -99,27 +99,23 @@ defmodule <%= inspect schema.module %>Token do
   @doc """
   Checks if the token is valid and returns its underlying lookup query.
 
-  The query returns the <%= schema.singular %> found by the token, if any.
+  If found, the query returns a tuple of the form `{<%= schema.singular %>, token}`.
 
   The given token is valid if it matches its hashed counterpart in the
-  database and the user email has not changed. This function also checks
-  if the token is being used within a certain period, depending on the
-  context. The default contexts supported by this function are either
-  "confirm", for account confirmation emails, and "reset_password",
-  for resetting the password. For verifying requests to change the email,
-  see `verify_change_email_token_query/2`.
+  database. This function also checks if the token is being used within
+  15 minutes. The context of a magic link token is always "login".
   """
-  def verify_email_token_query(token, context) do
+  def verify_magic_link_token_query(token) do
     case Base.url_decode64(token, padding: false) do
       {:ok, decoded_token} ->
         hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
-        days = days_for_context(context)
 
         query =
-          from token in by_token_and_context_query(hashed_token, context),
+          from token in by_token_and_context_query(hashed_token, "login"),
             join: <%= schema.singular %> in assoc(token, :<%= schema.singular %>),
-            where: token.inserted_at > ago(^days, "day") and token.sent_to == <%= schema.singular %>.email,
-            select: <%= schema.singular %>
+            where: token.inserted_at > ago(^@magic_link_validity_in_minutes, "minute"),
+            where: token.sent_to == <%= schema.singular %>.email,
+            select: {<%= schema.singular %>, token}
 
         {:ok, query}
 
@@ -128,19 +124,13 @@ defmodule <%= inspect schema.module %>Token do
     end
   end
 
-  defp days_for_context("confirm"), do: @confirm_validity_in_days
-  defp days_for_context("reset_password"), do: @reset_password_validity_in_days
-
   @doc """
   Checks if the token is valid and returns its underlying lookup query.
 
   The query returns the <%= schema.singular %>_token found by the token, if any.
 
   This is used to validate requests to change the <%= schema.singular %>
-  email. It is different from `verify_email_token_query/2` precisely because
-  `verify_email_token_query/2` validates the email has not changed, which is
-  the starting point by this function.
-
+  email.
   The given token is valid if it matches its hashed counterpart in the
   database and if it has not expired (after @change_email_validity_in_days).
   The context must always start with "change:".
@@ -178,4 +168,11 @@ defmodule <%= inspect schema.module %>Token do
   def by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [_ | _] = contexts) do
     from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id and t.context in ^contexts
   end
+
+  @doc """
+  Deletes a list of tokens.
+  """
+  def delete_all_query(tokens) do
+    from t in <%= inspect schema.alias %>Token, where: t.id in ^Enum.map(tokens, & &1.id)
+  end
 end
diff --git a/priv/templates/phx.gen.auth/session_confirm.html.heex b/priv/templates/phx.gen.auth/session_confirm.html.heex
new file mode 100644
index 000000000..505adbd20
--- /dev/null
+++ b/priv/templates/phx.gen.auth/session_confirm.html.heex
@@ -0,0 +1,38 @@
+<div class="mx-auto max-w-sm">
+  <.header class="text-center">Welcome {@<%= schema.singular %>.email}</.header>
+
+  <.simple_form
+    :if={!@<%= schema.singular %>.confirmed_at}
+    for={@form}
+    id="confirmation_form"
+    action={~p"<%= schema.route_prefix %>/log-in?_action=confirmed"}
+  >
+    <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+    <.input
+      :if={!@current_<%= schema.singular %>}
+      field={@form[:remember_me]}
+      type="checkbox"
+      label="Keep me logged in"
+    />
+    <:actions>
+      <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
+    </:actions>
+  </.simple_form>
+
+  <.simple_form :if={@<%= schema.singular %>.confirmed_at} for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"}>
+    <input type="hidden" name={@form[:token].name} value={@form[:token].value} />
+    <.input
+      :if={!@current_<%= schema.singular %>}
+      field={@form[:remember_me]}
+      type="checkbox"
+      label="Keep me logged in"
+    />
+    <:actions>
+      <.button phx-disable-with="Logging in..." class="w-full">Log in</.button>
+    </:actions>
+  </.simple_form>
+
+  <p :if={!@<%= schema.singular %>.confirmed_at} class="mt-8 p-4 border text-center">
+    Tip: If you prefer passwords, you can enable them in the <%= schema.singular %> settings.
+  </p>
+</div>
diff --git a/priv/templates/phx.gen.auth/session_controller.ex b/priv/templates/phx.gen.auth/session_controller.ex
index bab413a3c..f9b4d1460 100644
--- a/priv/templates/phx.gen.auth/session_controller.ex
+++ b/priv/templates/phx.gen.auth/session_controller.ex
@@ -4,20 +4,32 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   alias <%= inspect context.module %>
   alias <%= inspect auth_module %><%= if live? do %>
 
-  def create(conn, %{"_action" => "registered"} = params) do
-    create(conn, params, "Account created successfully!")
-  end
-
-  def create(conn, %{"_action" => "password-updated"} = params) do
-    conn
-    |> put_session(:<%= schema.singular %>_return_to, ~p"<%= schema.route_prefix %>/settings")
-    |> create(params, "Password updated successfully!")
+  def create(conn, %{"_action" => "confirmed"} = params) do
+    create(conn, params, "<%= schema.human_singular %> confirmed successfully.")
   end
 
   def create(conn, params) do
     create(conn, params, "Welcome back!")
   end
 
+  # magic link login
+  defp create(conn, %{"<%= schema.singular %>" => %{"token" => token} = <%= schema.singular %>_params}, info) do
+    case <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(token) do
+      {:ok, <%= schema.singular %>, tokens_to_disconnect} ->
+        <%= inspect schema.alias %>Auth.disconnect_sessions(tokens_to_disconnect)
+
+        conn
+        |> put_flash(:info, info)
+        |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params)
+
+      _ ->
+        conn
+        |> put_flash(:error, "The link is invalid or it has expired.")
+        |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
+    end
+  end
+
+  # email + password login
   defp create(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params}, info) do
     %{"email" => email, "password" => password} = <%= schema.singular %>_params
 
@@ -32,22 +44,95 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       |> put_flash(:email, String.slice(email, 0, 160))
       |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
     end
+  end
+
+  def update_password(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params) do
+    <%= schema.singular %> = conn.assigns.current_<%= schema.singular %>
+    true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
+    {:ok, _<%= schema.singular %>, expired_tokens} = <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params)
+
+    # disconnect all existing LiveViews with old sessions
+    <%= inspect schema.alias %>Auth.disconnect_sessions(expired_tokens)
+
+    conn
+    |> put_session(:<%= schema.singular %>_return_to, ~p"<%= schema.route_prefix %>/settings")
+    |> create(params, "Password updated successfully!")
   end<% else %>
 
   def new(conn, _params) do
-    render(conn, :new, error_message: nil)
+    email = get_in(conn.assigns, [:current_<%= schema.singular %>, Access.key(:email)])
+    form = Phoenix.Component.to_form(%{"email" => email}, as: "<%= schema.singular %>")
+
+    render(conn, :new, form: form, error_message: nil)
   end
 
-  def create(conn, %{"<%= schema.singular %>" => <%= schema.singular %>_params}) do
-    %{"email" => email, "password" => password} = <%= schema.singular %>_params
+  # magic link login
+  def create(conn, %{"<%= schema.singular %>" => %{"token" => token} = <%= schema.singular %>_params} = params) do
+    info =
+      case params do
+        %{"_action" => "confirmed"} -> "<%= schema.human_singular %> confirmed successfully."
+        _ -> "Welcome back!"
+      end
 
+    case <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(token) do
+      {:ok, <%= schema.singular %>, _expired_tokens} ->
+        conn
+        |> put_flash(:info, info)
+        |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params)
+
+      {:error, :not_found} ->
+        conn
+        |> put_flash(:error, "The link is invalid or it has expired.")
+        |> render(:new,
+          form: Phoenix.Component.to_form(%{}, as: "<%= schema.singular %>"),
+          error_message: nil
+        )
+    end
+  end
+
+  # email + password login
+  def create(conn, %{"<%= schema.singular %>" => %{"email" => email, "password" => password} = <%= schema.singular %>_params}) do
     if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(email, password) do
       conn
       |> put_flash(:info, "Welcome back!")
       |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params)
     else
+      form = Phoenix.Component.to_form(<%= schema.singular %>_params, as: "<%= schema.singular %>")
+
       # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
-      render(conn, :new, error_message: "Invalid email or password")
+      render(conn, :new, form: form, error_message: "Invalid email or password")
+    end
+  end
+
+  # magic link request
+  def create(conn, %{"<%= schema.singular %>" => %{"email" => email}}) do
+    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
+      <%= inspect context.alias %>.deliver_login_instructions(
+        <%= schema.singular %>,
+        &url(~p"<%= schema.route_prefix %>/log-in/#{&1}")
+      )
+    end
+
+    info =
+      "If your email is in our system, you will receive instructions for logging in shortly."
+
+    conn
+    |> put_flash(:info, info)
+    |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
+  end
+
+  def confirm(conn, %{"token" => token}) do
+    if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_magic_link_token(token) do
+      form = Phoenix.Component.to_form(%{"token" => token}, as: "<%= schema.singular %>")
+
+      conn
+      |> assign(:<%= schema.singular %>, <%= schema.singular %>)
+      |> assign(:form, form)
+      |> render(:confirm)
+    else
+      conn
+      |> put_flash(:error, "Magic link is invalid or it has expired.")
+      |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
     end
   end<% end %>
 
diff --git a/priv/templates/phx.gen.auth/session_html.ex b/priv/templates/phx.gen.auth/session_html.ex
index d04299242..0a02f3824 100644
--- a/priv/templates/phx.gen.auth/session_html.ex
+++ b/priv/templates/phx.gen.auth/session_html.ex
@@ -2,4 +2,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   use <%= inspect context.web_module %>, :html
 
   embed_templates "<%= schema.singular %>_session_html/*"
+
+  defp local_mail_adapter? do
+    Application.get_env(:<%= Mix.Phoenix.otp_app() %>, <%= inspect context.base_module %>.Mailer)[:adapter] == Swoosh.Adapters.Local
+  end
 end
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index 8e19c7401..57af1db0f 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -1,37 +1,71 @@
 <div class="mx-auto max-w-sm">
   <.header class="text-center">
-    Log in to account
+    <p>Log in</p>
     <:subtitle>
-      Don't have an account?
-      <.link navigate={~p"<%= schema.route_prefix %>/register"} class="font-semibold text-brand hover:underline">
-        Sign up
-      </.link>
-      for an account now.
+      <%%= if @current_<%= schema.singular %> do %>
+        You need to reauthenticate to perform sensitive actions on your account.
+      <%% else %>
+        Don't have an account? <.link
+          navigate={~p"<%= schema.route_prefix %>/register"}
+          class="font-semibold text-brand hover:underline"
+          phx-no-format
+        >Sign up</.link> for an account now.
+      <%% end %>
     </:subtitle>
   </.header>
 
-  <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log-in"}>
-    <.error :if={@error_message}>{@error_message}</.error>
-
-    <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
+  <.simple_form :let={f} for={@form} as={:<%= schema.singular %>} id="login_form_magic" action={~p"<%= schema.route_prefix %>/log-in"}>
     <.input
-      field={f[:password]}
-      type="password"
-      label="Password"
-      autocomplete="current-password"
+      readonly={!!@current_<%= schema.singular %>}
+      field={f[:email]}
+      type="email"
+      label="Email"
+      autocomplete="username"
       required
     />
+    <.button class="w-full">
+      Log in with email <span aria-hidden="true">→</span>
+    </.button>
+  </.simple_form>
 
-    <:actions :let={f}>
-      <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
-      <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
-        Forgot your password?
-      </.link>
-    </:actions>
-    <:actions>
-      <.button phx-disable-with="Logging in..." class="w-full">
-        Log in <span aria-hidden="true">→</span>
-      </.button>
-    </:actions>
+  <div class="flex items-center mt-8 -mb-6 text-sm">
+    <hr class="flex-grow border-t-2 border-gray-300" />
+    <span class="mx-2 text-gray-500">or</span>
+    <hr class="flex-grow border-t-2 border-gray-300" />
+  </div>
+
+  <.simple_form
+    :let={f}
+    for={@form}
+    as={:<%= schema.singular %>}
+    id="login_form_password"
+    action={~p"<%= schema.route_prefix %>/log-in"}
+  >
+    <.error :if={@error_message}>{@error_message}</.error>
+    <.input
+      readonly={!!@current_<%= schema.singular %>}
+      field={f[:email]}
+      type="email"
+      label="Email"
+      autocomplete="username"
+      required
+    />
+    <.input field={f[:password]} type="password" label="Password" autocomplete="current-password" />
+    <.input
+      :if={!@current_<%= schema.singular %>}
+      field={f[:remember_me]}
+      type="checkbox"
+      label="Keep me logged in"
+    />
+    <.button class="w-full">
+      Log in <span aria-hidden="true">→</span>
+    </.button>
   </.simple_form>
+
+  <div :if={local_mail_adapter?()} class="mt-8 p-4 border text-center">
+    <p>You are running the local mail adapter.</p>
+    <p>
+      To see sent emails, visit <.link href="/dev/mailbox" class="underline">the mailbox page</.link>.
+    </p>
+  </div>
 </div>
diff --git a/priv/templates/phx.gen.auth/settings_controller.ex b/priv/templates/phx.gen.auth/settings_controller.ex
index c046cf779..221a253b3 100644
--- a/priv/templates/phx.gen.auth/settings_controller.ex
+++ b/priv/templates/phx.gen.auth/settings_controller.ex
@@ -4,6 +4,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   alias <%= inspect context.module %>
   alias <%= inspect auth_module %>
 
+  import <%= inspect auth_module %>, only: [require_sudo_mode: 2]
+
+  plug :require_sudo_mode
   plug :assign_email_and_password_changesets
 
   def edit(conn, _params) do
@@ -11,13 +14,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def update(conn, %{"action" => "update_email"} = params) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
     <%= schema.singular %> = conn.assigns.current_<%= schema.singular %>
 
-    case <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, password, <%= schema.singular %>_params) do
-      {:ok, applied_<%= schema.singular %>} ->
+    case <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, <%= schema.singular %>_params) do
+      %{valid?: true} = changeset ->
         <%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
-          applied_<%= schema.singular %>,
+          Ecto.Changeset.apply_action!(changeset, :insert),
           <%= schema.singular %>.email,
           &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
         )
@@ -29,17 +32,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         )
         |> redirect(to: ~p"<%= schema.route_prefix %>/settings")
 
-      {:error, changeset} ->
-        render(conn, :edit, email_changeset: changeset)
+      changeset ->
+        render(conn, :edit, email_changeset: %{changeset | action: :insert})
     end
   end
 
   def update(conn, %{"action" => "update_password"} = params) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
     <%= schema.singular %> = conn.assigns.current_<%= schema.singular %>
 
-    case <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, password, <%= schema.singular %>_params) do
-      {:ok, <%= schema.singular %>} ->
+    case <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params) do
+      {:ok, <%= schema.singular %>, _} ->
         conn
         |> put_flash(:info, "Password updated successfully.")
         |> put_session(:<%= schema.singular %>_return_to, ~p"<%= schema.route_prefix %>/settings")
diff --git a/priv/templates/phx.gen.auth/settings_edit.html.heex b/priv/templates/phx.gen.auth/settings_edit.html.heex
index e741536a9..78c49da9a 100644
--- a/priv/templates/phx.gen.auth/settings_edit.html.heex
+++ b/priv/templates/phx.gen.auth/settings_edit.html.heex
@@ -13,15 +13,6 @@
       <input type="hidden" name="action" value="update_email" />
 
       <.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
-      <.input
-        field={f[:current_password]}
-        name="current_password"
-        type="password"
-        label="Current Password"
-        autocomplete="current-password"
-        required
-        id="current_password_for_email"
-      />
       <:actions>
         <.button phx-disable-with="Changing...">Change Email</.button>
       </:actions>
@@ -54,18 +45,10 @@
         autocomplete="new-password"
         required
       />
-
-      <.input
-        field={f[:current_password]}
-        name="current_password"
-        type="password"
-        label="Current password"
-        id="current_password_for_password"
-        autocomplete="current-password"
-        required
-      />
       <:actions>
-        <.button phx-disable-with="Changing...">Change Password</.button>
+        <.button phx-disable-with="Changing...">
+          Save Password
+        </.button>
       </:actions>
     </.simple_form>
   </div>
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index 7dc7c999b..ed81c6e6b 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -1,6 +1,8 @@
 defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Settings do
   use <%= inspect context.web_module %>, :live_view
 
+  on_mount {<%= inspect auth_module %>, :ensure_sudo_mode}
+
   alias <%= inspect context.module %>
 
   def render(assigns) do
@@ -25,16 +27,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             autocomplete="username"
             required
           />
-          <.input
-            field={@email_form[:current_password]}
-            name="current_password"
-            id="current_password_for_email"
-            type="password"
-            label="Current password"
-            value={@email_form_current_password}
-            autocomplete="current-password"
-            required
-          />
           <:actions>
             <.button phx-disable-with="Changing...">Change Email</.button>
           </:actions>
@@ -44,7 +36,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         <.simple_form
           for={@password_form}
           id="password_form"
-          action={~p"<%= schema.route_prefix %>/log-in?_action=password-updated"}
+          action={~p"<%= schema.route_prefix %>/update-password"}
           method="post"
           phx-change="validate_password"
           phx-submit="update_password"
@@ -70,18 +62,10 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
             label="Confirm new password"
             autocomplete="new-password"
           />
-          <.input
-            field={@password_form[:current_password]}
-            name="current_password"
-            type="password"
-            label="Current password"
-            id="current_password_for_password"
-            value={@current_password}
-            autocomplete="current-password"
-            required
-          />
           <:actions>
-            <.button phx-disable-with="Changing...">Change Password</.button>
+            <.button phx-disable-with="Saving...">
+              Save Password
+            </.button>
           </:actions>
         </.simple_form>
       </div>
@@ -104,13 +88,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def mount(_params, _session, socket) do
     <%= schema.singular %> = socket.assigns.current_<%= schema.singular %>
-    email_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>)
-    password_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>)
+    email_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, %{}, validate_email: false)
+    password_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>, %{}, hash_password: false)
 
     socket =
       socket
-      |> assign(:current_password, nil)
-      |> assign(:email_form_current_password, nil)
       |> assign(:current_email, <%= schema.singular %>.email)
       |> assign(:email_form, to_form(email_changeset))
       |> assign(:password_form, to_form(password_changeset))
@@ -120,64 +102,61 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def handle_event("validate_email", params, socket) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
 
     email_form =
       socket.assigns.current_<%= schema.singular %>
-      |> <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>_params)
+      |> <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>_params, validate_email: false)
       |> Map.put(:action, :validate)
       |> to_form()
 
-    {:noreply, assign(socket, email_form: email_form, email_form_current_password: password)}
+    {:noreply, assign(socket, email_form: email_form)}
   end
 
   def handle_event("update_email", params, socket) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
     <%= schema.singular %> = socket.assigns.current_<%= schema.singular %>
+    true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
 
-    case <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, password, <%= schema.singular %>_params) do
-      {:ok, applied_<%= schema.singular %>} ->
+    case <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>, <%= schema.singular %>_params) do
+      %{valid?: true} = changeset ->
         <%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
-          applied_<%= schema.singular %>,
+          Ecto.Changeset.apply_action!(changeset, :insert),
           <%= schema.singular %>.email,
           &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
         )
 
         info = "A link to confirm your email change has been sent to the new address."
-        {:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
+        {:noreply, socket |> put_flash(:info, info)}
 
-      {:error, changeset} ->
-        {:noreply, assign(socket, :email_form, to_form(Map.put(changeset, :action, :insert)))}
+      changeset ->
+        {:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))}
     end
   end
 
   def handle_event("validate_password", params, socket) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
 
     password_form =
       socket.assigns.current_<%= schema.singular %>
-      |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params)
+      |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params, hash_password: false)
       |> Map.put(:action, :validate)
       |> to_form()
 
-    {:noreply, assign(socket, password_form: password_form, current_password: password)}
+    {:noreply, assign(socket, password_form: password_form)}
   end
 
   def handle_event("update_password", params, socket) do
-    %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
+    %{"<%= schema.singular %>" => <%= schema.singular %>_params} = params
     <%= schema.singular %> = socket.assigns.current_<%= schema.singular %>
+    true = <%= inspect context.alias %>.sudo_mode?(<%= schema.singular %>)
 
-    case <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, password, <%= schema.singular %>_params) do
-      {:ok, <%= schema.singular %>} ->
-        password_form =
-          <%= schema.singular %>
-          |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params)
-          |> to_form()
-
-        {:noreply, assign(socket, trigger_submit: true, password_form: password_form)}
+    case <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params) do
+      %{valid?: true} = changeset ->
+        {:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))}
 
-      {:error, changeset} ->
-        {:noreply, assign(socket, password_form: to_form(changeset))}
+      changeset ->
+        {:noreply, assign(socket, password_form: to_form(changeset, action: :insert))}
     end
   end
 end
diff --git a/priv/templates/phx.gen.context/access_no_schema.ex b/priv/templates/phx.gen.context/access_no_schema.ex
index 71afc7768..66b765e91 100644
--- a/priv/templates/phx.gen.context/access_no_schema.ex
+++ b/priv/templates/phx.gen.context/access_no_schema.ex
@@ -25,7 +25,7 @@
       %<%= inspect schema.alias %>{}
 
   """
-  def get_<%= schema.singular %>!(id), do: raise "TODO"
+  def get_<%= schema.singular %>!(<%= primary_key %>), do: raise "TODO"
 
   @doc """
   Creates a <%= schema.singular %>.
diff --git a/priv/templates/phx.gen.context/schema_access.ex b/priv/templates/phx.gen.context/schema_access.ex
index bdee344d5..9bedfd035 100644
--- a/priv/templates/phx.gen.context/schema_access.ex
+++ b/priv/templates/phx.gen.context/schema_access.ex
@@ -28,7 +28,7 @@
       ** (Ecto.NoResultsError)
 
   """
-  def get_<%= schema.singular %>!(id), do: Repo.get!(<%= inspect schema.alias %>, id)
+  def get_<%= schema.singular %>!(<%= primary_key %>), do: Repo.get!(<%= inspect schema.alias %>, <%= primary_key %>)
 
   @doc """
   Creates a <%= schema.singular %>.
diff --git a/priv/templates/phx.gen.html/controller.ex b/priv/templates/phx.gen.html/controller.ex
index de3e013ed..2506a0738 100644
--- a/priv/templates/phx.gen.html/controller.ex
+++ b/priv/templates/phx.gen.html/controller.ex
@@ -26,19 +26,19 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  def show(conn, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def show(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
     render(conn, :show, <%= schema.singular %>: <%= schema.singular %>)
   end
 
-  def edit(conn, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def edit(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
     changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)
     render(conn, :edit, <%= schema.singular %>: <%= schema.singular %>, changeset: changeset)
   end
 
-  def update(conn, %{"id" => id, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def update(conn, %{"<%= primary_key %>" => <%= primary_key %>, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
 
     case <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params) do
       {:ok, <%= schema.singular %>} ->
@@ -51,8 +51,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  def delete(conn, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def delete(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
     {:ok, _<%= schema.singular %>} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
 
     conn
diff --git a/priv/templates/phx.gen.html/edit.html.heex b/priv/templates/phx.gen.html/edit.html.heex
index a896b5d79..1af943b41 100644
--- a/priv/templates/phx.gen.html/edit.html.heex
+++ b/priv/templates/phx.gen.html/edit.html.heex
@@ -1,5 +1,5 @@
 <.header>
-  Edit <%= schema.human_singular %> {@<%= schema.singular %>.id}
+  Edit <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
   <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
 </.header>
 
diff --git a/priv/templates/phx.gen.html/show.html.heex b/priv/templates/phx.gen.html/show.html.heex
index a9e15ad98..f1bdeaacc 100644
--- a/priv/templates/phx.gen.html/show.html.heex
+++ b/priv/templates/phx.gen.html/show.html.heex
@@ -1,5 +1,5 @@
 <.header>
-  <%= schema.human_singular %> {@<%= schema.singular %>.id}
+  <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
   <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
   <:actions>
     <.link class={button_classes()} href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
diff --git a/priv/templates/phx.gen.json/controller.ex b/priv/templates/phx.gen.json/controller.ex
index ff29d7952..4e82dc5a0 100644
--- a/priv/templates/phx.gen.json/controller.ex
+++ b/priv/templates/phx.gen.json/controller.ex
@@ -20,21 +20,21 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
   end
 
-  def show(conn, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def show(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
     render(conn, :show, <%= schema.singular %>: <%= schema.singular %>)
   end
 
-  def update(conn, %{"id" => id, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def update(conn, %{"<%= primary_key %>" => <%= primary_key %>, <%= inspect schema.singular %> => <%= schema.singular %>_params}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
 
     with {:ok, %<%= inspect schema.alias %>{} = <%= schema.singular %>} <- <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, <%= schema.singular %>_params) do
       render(conn, :show, <%= schema.singular %>: <%= schema.singular %>)
     end
   end
 
-  def delete(conn, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def delete(conn, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
 
     with {:ok, %<%= inspect schema.alias %>{}} <- <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>) do
       send_resp(conn, :no_content, "")
diff --git a/priv/templates/phx.gen.json/json.ex b/priv/templates/phx.gen.json/json.ex
index 5cff79e99..d71a684de 100644
--- a/priv/templates/phx.gen.json/json.ex
+++ b/priv/templates/phx.gen.json/json.ex
@@ -17,7 +17,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   defp data(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
     %{
-<%= [{:id, :id} | schema.attrs] |> Enum.map(fn {k, _} -> "      #{k}: #{schema.singular}.#{k}" end) |> Enum.join(",\n")  %>
+<%= [{primary_key, :id} | schema.attrs] |> Enum.map(fn {k, _} -> "      #{k}: #{schema.singular}.#{k}" end) |> Enum.join(",\n")  %>
     }
   end
 end
diff --git a/priv/templates/phx.gen.live/form.ex b/priv/templates/phx.gen.live/form.ex
index c9ea3be8b..91576e3d2 100644
--- a/priv/templates/phx.gen.live/form.ex
+++ b/priv/templates/phx.gen.live/form.ex
@@ -34,8 +34,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   defp return_to("show"), do: "show"
   defp return_to(_), do: "index"
 
-  defp apply_action(socket, :edit, %{"id" => id}) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  defp apply_action(socket, :edit, %{"<%= primary_key %>" => <%= primary_key %>}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
 
     socket
     |> assign(:page_title, "Edit <%= schema.human_singular %>")
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index 8c16389d7..a5ec14bab 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </:action>
       <:action :let={{id, <%= schema.singular %>}}>
         <.link
-          phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}) |> hide("##{id}")}
+          phx-click={JS.push("delete", value: %{<%= primary_key %>: <%= schema.singular %>.<%= primary_key %>}) |> hide("##{id}")}
           data-confirm="Are you sure?"
         >
           Delete
@@ -43,14 +43,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def mount(_params, _session, socket) do
     {:ok,
      socket
-     |> assign(:page_title, "Listing <%= schema.human_plural %>")<%= if schema.opts[:primary_key] do %>
-     |> stream_configure(:<%= schema.collection %>, dom_id: &"<%= schema.table %>-#{&1.<%= schema.opts[:primary_key] %>}")<% end %>
+     |> assign(:page_title, "Listing <%= schema.human_plural %>")<%= if primary_key != :id do %>
+     |> stream_configure(:<%= schema.collection %>, dom_id: &"<%= schema.table %>-#{&1.<%= primary_key %>}")<% end %>
      |> stream(:<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
   end
 
   @impl true
-  def handle_event("delete", %{"id" => id}, socket) do
-    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+  def handle_event("delete", %{"<%= primary_key %>" => <%= primary_key %>}, socket) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>)
     {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
 
     {:noreply, stream_delete(socket, :<%= schema.collection %>, <%= schema.singular %>)}
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index e5c449b0a..d344bd6e8 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -7,7 +7,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def render(assigns) do
     ~H"""
     <.header>
-      <%= schema.human_singular %> {@<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}
+      <%= schema.human_singular %> {@<%= schema.singular %>.<%= primary_key %>}
       <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
       <:actions>
         <.link class={button_classes()} navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
@@ -25,10 +25,10 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   @impl true
-  def mount(%{"id" => id}, _session, socket) do
+  def mount(%{"<%= primary_key %>" => <%= primary_key %>}, _session, socket) do
     {:ok,
      socket
      |> assign(:page_title, "Show <%= schema.human_singular %>")
-     |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))}
+     |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= primary_key %>))}
   end
 end
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index 7b3fcc3a1..aea60ed6b 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -3,7 +3,7 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
 
   def change do
     create table(:<%= schema.table %><%= if schema.binary_id || schema.opts[:primary_key] do %>, primary_key: false<% end %><%= if schema.prefix do %>, prefix: :<%= schema.prefix %><% end %>) do
-<%= if schema.binary_id do %>      add :<%= schema.opts[:primary_key] || :id %>, :binary_id, primary_key: true
+<%= if schema.binary_id do %>      add :<%= primary_key %>, :binary_id, primary_key: true
 <% else %><%= if schema.opts[:primary_key] do %>      add :<%= schema.opts[:primary_key] %>, :id, primary_key: true
 <% end %><% end %><%= for {k, v} <- schema.attrs do %>      add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
 <% end %><%= for {_, i, _, s} <- schema.assocs do %>      add <%= inspect(i) %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index 082b3f43d..1c37d6656 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -4,7 +4,7 @@ defmodule <%= inspect schema.module %> do
 <%= if schema.prefix do %>
   @schema_prefix :<%= schema.prefix %><% end %><%= if schema.opts[:primary_key] do %>
   @derive {Phoenix.Param, key: :<%= schema.opts[:primary_key] %>}<% end %><%= if schema.binary_id do %>
-  @primary_key {:<%= schema.opts[:primary_key] || :id %>, :binary_id, autogenerate: true}
+  @primary_key {:<%= primary_key %>, :binary_id, autogenerate: true}
   @foreign_key_type :binary_id<% else %><%= if schema.opts[:primary_key] do %>
   @primary_key {:<%= schema.opts[:primary_key] %>, :id, autogenerate: true}<% end %><% end %>
   schema <%= inspect schema.table %> do


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index baaeeb328..73487e14e 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -704,7 +704,7 @@ defmodule Mix.Tasks.Phx.NewTest do
         assert file =~ "skip: skip_migrations?()"
 
         assert file =~ "defp skip_migrations?() do"
-        assert file =~ ~s/System.get_env("RELEASE_NAME") != nil/
+        assert file =~ ~s/System.get_env("RELEASE_NAME") == nil/
       end)
 
       assert_file("custom_path/test/support/conn_case.ex", "DataCase.setup_sandbox(tags)")
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 31a95ed3a..1b97c6fdf 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -635,7 +635,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert file =~ "skip: skip_migrations?()"
 
         assert file =~ "defp skip_migrations?() do"
-        assert file =~ ~s/System.get_env("RELEASE_NAME") != nil/
+        assert file =~ ~s/System.get_env("RELEASE_NAME") == nil/
       end)
 
       assert_file(root_path(app, "config/dev.exs"), [~r/database: .*_dev.db/])
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index b88173a9c..94815f33c 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -14,14 +14,14 @@ defmodule <%= inspect auth_module %>Test do
       |> Map.replace!(:secret_key_base, <%= inspect endpoint_module %>.config(:secret_key_base))
       |> init_test_session(%{})
 
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture(), conn: conn}
+    %{<%= schema.singular %>: %{<%= schema.singular %>_fixture() | authenticated_at: <%= inspect datetime_module %>.utc_now()}, conn: conn}
   end
 
   describe "log_in_<%= schema.singular %>/3" do
     test "stores the <%= schema.singular %> token in the session", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn = <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(conn, <%= schema.singular %>)
-      assert token = get_session(conn, :<%= schema.singular %>_token)
-      assert get_session(conn, :live_socket_id) == "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}"
+      assert token = get_session(conn, :<%= schema.singular %>_token)<%= if live? do %>
+      assert get_session(conn, :live_socket_id) == "<%= schema.plural %>_sessions:#{Base.url_encode64(token)}"<% end %>
       assert redirected_to(conn) == ~p"/"
       assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(token)
     end
@@ -39,10 +39,38 @@ defmodule <%= inspect auth_module %>Test do
     test "writes a cookie if remember_me is configured", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn = conn |> fetch_cookies() |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, %{"remember_me" => "true"})
       assert get_session(conn, :<%= schema.singular %>_token) == conn.cookies[@remember_me_cookie]
+      assert get_session(conn, :<%= schema.singular %>_remember_me) == true
 
       assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
       assert signed_token != get_session(conn, :<%= schema.singular %>_token)
       assert max_age == 5_184_000
+    end<%= if live? do %>
+
+    test "redirects to settings when <%= schema.singular %> is already logged in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      conn = conn |> assign(:current_<%= schema.singular %>, <%= schema.singular %>) |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>)
+      assert redirected_to(conn) == "<%= schema.route_prefix %>/settings"
+    end<% end %>
+
+    test "writes a cookie if remember_me was set in previous session", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      conn = conn |> fetch_cookies() |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, %{"remember_me" => "true"})
+      assert get_session(conn, :<%= schema.singular %>_token) == conn.cookies[@remember_me_cookie]
+      assert get_session(conn, :<%= schema.singular %>_remember_me) == true
+
+      conn =
+        conn
+        |> recycle()
+        |> Map.replace!(:secret_key_base, <%= inspect endpoint_module %>.config(:secret_key_base))
+        |> fetch_cookies()
+        |> init_test_session(%{<%= schema.singular %>_remember_me: true})
+
+      # the conn is already logged in and has the remeber_me cookie set,
+      # now we log in again and even without explicitly setting remember_me,
+      # the cookie should be set again
+      conn = conn |> <%= inspect schema.alias %>Auth.log_in_<%= schema.singular %>(<%= schema.singular %>, %{})
+      assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
+      assert signed_token != get_session(conn, :<%= schema.singular %>_token)
+      assert max_age == 5_184_000
+      assert get_session(conn, :<%= schema.singular %>_remember_me) == true
     end
   end
 
@@ -64,7 +92,7 @@ defmodule <%= inspect auth_module %>Test do
       refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
     end
 
-    test "broadcasts to the given live_socket_id", %{conn: conn} do
+    <%= if live? do %>test "broadcasts to the given live_socket_id", %{conn: conn} do
       live_socket_id = "<%= schema.plural %>_sessions:abcdef-token"
       <%= inspect(endpoint_module) %>.subscribe(live_socket_id)
 
@@ -75,7 +103,7 @@ defmodule <%= inspect auth_module %>Test do
       assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
     end
 
-    test "works even if <%= schema.singular %> is already logged out", %{conn: conn} do
+    <% end %>test "works even if <%= schema.singular %> is already logged out", %{conn: conn} do
       conn = conn |> fetch_cookies() |> <%= inspect schema.alias %>Auth.log_out_<%= schema.singular %>()
       refute get_session(conn, :<%= schema.singular %>_token)
       assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
@@ -103,10 +131,10 @@ defmodule <%= inspect auth_module %>Test do
         |> <%= inspect schema.alias %>Auth.fetch_current_<%= schema.singular %>([])
 
       assert conn.assigns.current_<%= schema.singular %>.id == <%= schema.singular %>.id
-      assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token
+      assert get_session(conn, :<%= schema.singular %>_token) == <%= schema.singular %>_token<%= if live? do %>
 
       assert get_session(conn, :live_socket_id) ==
-               "<%= schema.plural %>_sessions:#{Base.url_encode64(<%= schema.singular %>_token)}"
+               "<%= schema.plural %>_sessions:#{Base.url_encode64(<%= schema.singular %>_token)}"<% end %>
     end
 
     test "does not authenticate if data is missing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
@@ -185,34 +213,64 @@ defmodule <%= inspect auth_module %>Test do
     end
   end
 
-  describe "on_mount :redirect_if_<%= schema.singular %>_is_authenticated" do
-    test "redirects if there is an authenticated  <%= schema.singular %> ", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+  describe "on_mount :ensure_sudo_mode" do
+    test "allows <%= schema.plural %> that have authenticated in the last 10 minutes", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       <%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
       session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
 
+      socket = %LiveView.Socket{
+        endpoint: <%= inspect(endpoint_module) %>,
+        assigns: %{__changed__: %{}, flash: %{}}
+      }
+
+      assert {:cont, _updated_socket} =
+               <%= inspect schema.alias %>Auth.on_mount(:ensure_sudo_mode, %{}, session, socket)
+    end
+
+    test "redirects when authentication is too old", %{<%= schema.singular %>: <%= schema.singular %>} do
+      eleven_minutes_ago = DateTime.utc_now() |> DateTime.add(-11, :minute)
+
+      socket = %LiveView.Socket{
+        endpoint: AuthAppWeb.Endpoint,
+        assigns: %{
+          __changed__: %{},
+          flash: %{},
+          current_<%= schema.singular %>: %{<%= schema.singular %> | authenticated_at: eleven_minutes_ago}
+        }
+      }
+
       assert {:halt, _updated_socket} =
-               <%= inspect schema.alias %>Auth.on_mount(
-                 :redirect_if_<%= schema.singular %>_is_authenticated,
-                 %{},
-                 session,
-                 %LiveView.Socket{}
-               )
+               <%= inspect schema.alias %>Auth.on_mount(:ensure_sudo_mode, %{}, %{}, socket)
     end
+  end<% else %>describe "require_sudo_mode/2" do
+    test "allows <%= schema.plural %> that have authenticated in the last 10 minutes", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      conn =
+        conn
+        |> fetch_flash()
+        |> assign(:current_<%= schema.singular %>, <%= schema.singular %>)
+        |> <%= inspect schema.alias %>Auth.require_sudo_mode([])
 
-    test "doesn't redirect if there is no authenticated <%= schema.singular %>", %{conn: conn} do
-      session = conn |> get_session()
+      refute conn.halted
+      refute conn.status
+    end
 
-      assert {:cont, _updated_socket} =
-               <%= inspect schema.alias %>Auth.on_mount(
-                 :redirect_if_<%= schema.singular %>_is_authenticated,
-                 %{},
-                 session,
-                 %LiveView.Socket{}
-               )
+    test "redirects when authentication is too old", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      eleven_minutes_ago = DateTime.utc_now() |> DateTime.add(-11, :minute)
+
+      conn =
+        conn
+        |> fetch_flash()
+        |> assign(:current_<%= schema.singular %>, %{<%= schema.singular %> | authenticated_at: eleven_minutes_ago})
+        |> <%= inspect schema.alias %>Auth.require_sudo_mode([])
+
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
+
+      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+               "You must re-authenticate to access this page."
     end
   end
 
-  <% end %>describe "redirect_if_<%= schema.singular %>_is_authenticated/2" do
+  describe "redirect_if_<%= schema.singular %>_is_authenticated/2" do
     test "redirects if <%= schema.singular %> is authenticated", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn = conn |> assign(:current_<%= schema.singular %>, <%= schema.singular %>) |> <%= inspect schema.alias %>Auth.redirect_if_<%= schema.singular %>_is_authenticated([])
       assert conn.halted
@@ -224,7 +282,7 @@ defmodule <%= inspect auth_module %>Test do
       refute conn.halted
       refute conn.status
     end
-  end
+  end<% end %>
 
   describe "require_authenticated_<%= schema.singular %>/2" do
     test "redirects if <%= schema.singular %> is not authenticated", %{conn: conn} do
@@ -268,5 +326,27 @@ defmodule <%= inspect auth_module %>Test do
       refute conn.halted
       refute conn.status
     end
-  end
+  end<%= if live? do %>
+
+  describe "disconnect_sessions/1" do
+    test "broadcasts disconnect messages for each token" do
+      tokens = [%{token: "token1"}, %{token: "token2"}]
+
+      for %{token: token} <- tokens do
+        <%= inspect context.web_module %>.Endpoint.subscribe("<%= schema.plural %>_sessions:#{Base.url_encode64(token)}")
+      end
+
+      <%= inspect schema.alias %>Auth.disconnect_sessions(tokens)
+
+      assert_receive %Phoenix.Socket.Broadcast{
+        event: "disconnect",
+        topic: "<%= schema.plural %>_sessions:dG9rZW4x"
+      }
+
+      assert_receive %Phoenix.Socket.Broadcast{
+        event: "disconnect",
+        topic: "<%= schema.plural %>_sessions:dG9rZW4y"
+      }
+    end
+  end<% end %>
 end
diff --git a/priv/templates/phx.gen.auth/confirmation_controller_test.exs b/priv/templates/phx.gen.auth/confirmation_controller_test.exs
deleted file mode 100644
index d38e11f88..000000000
--- a/priv/templates/phx.gen.auth/confirmation_controller_test.exs
+++ /dev/null
@@ -1,122 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationControllerTest do
-  use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
-
-  alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %><%= schema.repo_alias %>
-  import <%= inspect context.module %>Fixtures
-
-  setup do
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-  end
-
-  describe "GET <%= schema.route_prefix %>/confirm" do
-    test "renders the resend confirmation page", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/confirm")
-      response = html_response(conn, 200)
-      assert response =~ "Resend confirmation instructions"
-    end
-  end
-
-  describe "POST <%= schema.route_prefix %>/confirm" do
-    @tag :capture_log
-    test "sends a new confirmation token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn =
-        post(conn, ~p"<%= schema.route_prefix %>/confirm", %{
-          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
-        })
-
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.get_by!(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context == "confirm"
-    end
-
-    test "does not send confirmation token if <%= schema.human_singular %> is confirmed", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      Repo.update!(<%= inspect context.alias %>.<%= inspect schema.alias %>.confirm_changeset(<%= schema.singular %>))
-
-      conn =
-        post(conn, ~p"<%= schema.route_prefix %>/confirm", %{
-          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
-        })
-
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      refute Repo.get_by(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
-    end
-
-    test "does not send confirmation token if email is invalid", %{conn: conn} do
-      conn =
-        post(conn, ~p"<%= schema.route_prefix %>/confirm", %{
-          "<%= schema.singular %>" => %{"email" => "unknown@example.com"}
-        })
-
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
-    end
-  end
-
-  describe "GET <%= schema.route_prefix %>/confirm/:token" do
-    test "renders the confirmation page", %{conn: conn} do
-      token_path = ~p"<%= schema.route_prefix %>/confirm/some-token"
-      conn = get(conn, token_path)
-      response = html_response(conn, 200)
-      assert response =~ "Confirm account"
-
-      assert response =~ "action=\"#{token_path}\""
-    end
-  end
-
-  describe "POST <%= schema.route_prefix %>/confirm/:token" do
-    test "confirms the given token once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, url)
-        end)
-
-      conn = post(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "<%= schema.human_singular %> confirmed successfully"
-
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
-      refute get_session(conn, :<%= schema.singular %>_token)
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
-
-      # When not logged in
-      conn = post(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "<%= schema.human_singular %> confirmation link is invalid or it has expired"
-
-      # When logged in
-      conn =
-        build_conn()
-        |> log_in_<%= schema.singular %>(<%= schema.singular %>)
-        |> post(~p"<%= schema.route_prefix %>/confirm/#{token}")
-
-      assert redirected_to(conn) == ~p"/"
-      refute Phoenix.Flash.get(conn.assigns.flash, :error)
-    end
-
-    test "does not confirm email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn = post(conn, ~p"<%= schema.route_prefix %>/confirm/oops")
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "<%= schema.human_singular %> confirmation link is invalid or it has expired"
-
-      refute <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
deleted file mode 100644
index d8d1d2376..000000000
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
+++ /dev/null
@@ -1,67 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructionsTest do
-  use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
-
-  import Phoenix.LiveViewTest
-  import <%= inspect context.module %>Fixtures
-
-  alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %><%= schema.repo_alias %>
-
-  setup do
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-  end
-
-  describe "Resend confirmation" do
-    test "renders the resend confirmation page", %{conn: conn} do
-      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/confirm")
-      assert html =~ "Resend confirmation instructions"
-    end
-
-    test "sends a new confirmation token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm")
-
-      {:ok, conn} =
-        lv
-        |> form("#resend_confirmation_form", <%= schema.singular %>: %{email: <%= schema.singular %>.email})
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.get_by!(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context == "confirm"
-    end
-
-    test "does not send confirmation token if <%= schema.singular %> is confirmed", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      Repo.update!(<%= inspect context.alias %>.<%= inspect schema.alias %>.confirm_changeset(<%= schema.singular %>))
-
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm")
-
-      {:ok, conn} =
-        lv
-        |> form("#resend_confirmation_form", <%= schema.singular %>: %{email: <%= schema.singular %>.email})
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      refute Repo.get_by(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
-    end
-
-    test "does not send confirmation token if email is invalid", %{conn: conn} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm")
-
-      {:ok, conn} =
-        lv
-        |> form("#resend_confirmation_form", <%= schema.singular %>: %{email: "unknown@example.com"})
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index 0eac2e43c..ff4da7f1a 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -5,85 +5,101 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   import <%= inspect context.module %>Fixtures
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %><%= schema.repo_alias %>
 
   setup do
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
+    %{unconfirmed_<%= schema.singular %>: unconfirmed_<%= schema.singular %>_fixture(), confirmed_<%= schema.singular %>: <%= schema.singular %>_fixture()}
   end
 
   describe "Confirm <%= schema.singular %>" do
-    test "renders confirmation page", %{conn: conn} do
-      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/some-token")
-      assert html =~ "Confirm Account"
+    test "renders confirmation page for unconfirmed <%= schema.singular %>", %{conn: conn, unconfirmed_<%= schema.singular %>: <%= schema.singular %>} do
+      token =
+        extract_<%= schema.singular %>_token(fn url ->
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+        end)
+
+      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+      assert html =~ "Confirm my account"
+    end
+
+    test "renders login page for confirmed <%= schema.singular %>", %{conn: conn, confirmed_<%= schema.singular %>: <%= schema.singular %>} do
+      token =
+        extract_<%= schema.singular %>_token(fn url ->
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+        end)
+
+      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+      refute html =~ "Confirm my account"
+      assert html =~ "Log in"
     end
 
-    test "confirms the given token once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+    test "confirms the given token once", %{conn: conn, unconfirmed_<%= schema.singular %>: <%= schema.singular %>} do
       token =
         extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, url)
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
         end)
 
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
 
-      result =
-        lv
-        |> form("#confirmation_form")
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
+      form = form(lv, "#confirmation_form", %{"<%= schema.singular %>" => %{"token" => token}})
+      render_submit(form)
 
-      assert {:ok, conn} = result
+      conn = follow_trigger_action(form, conn)
 
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
                "<%= inspect schema.alias %> confirmed successfully"
 
       assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
-      refute get_session(conn, :<%= schema.singular %>_token)
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
+      # we are logged in now
+      assert get_session(conn, :<%= schema.singular %>_token)
+      assert redirected_to(conn) == ~p"/"
 
-      # when not logged in
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
+      # log out, new conn
+      conn = build_conn()
 
-      result =
-        lv
-        |> form("#confirmation_form")
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
+      {:ok, _lv, html} =
+        live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
-      assert {:ok, conn} = result
+      assert html =~ "Magic link is invalid or it has expired"
+    end
 
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
+    test "logs confirmed <%= schema.singular %> in without changing confirmed_at", %{
+      conn: conn,
+      confirmed_<%= schema.singular %>: <%= schema.singular %>
+    } do
+      token =
+        extract_<%= schema.singular %>_token(fn url ->
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+        end)
 
-      # when logged in
-      conn =
-        build_conn()
-        |> log_in_<%= schema.singular %>(<%= schema.singular %>)
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
 
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
+      form = form(lv, "#login_form", %{"<%= schema.singular %>" => %{"token" => token}})
+      render_submit(form)
 
-      result =
-        lv
-        |> form("#confirmation_form")
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
+      conn = follow_trigger_action(form, conn)
 
-      assert {:ok, conn} = result
-      refute Phoenix.Flash.get(conn.assigns.flash, :error)
-    end
+      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
+               "Welcome back!"
+
+      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at == <%= schema.singular %>.confirmed_at
 
-    test "does not confirm email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/invalid-token")
+      # log out, new conn
+      conn = build_conn()
 
-      {:ok, conn} =
-        lv
-        |> form("#confirmation_form")
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
+      {:ok, _lv, html} =
+        live(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      assert html =~ "Magic link is invalid or it has expired"
+    end
 
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
+    test "raises error for invalid token", %{conn: conn} do
+      {:ok, _lv, html} =
+        live(conn, ~p"<%= schema.route_prefix %>/log-in/invalid-token")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
-      refute <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
+      assert html =~ "Magic link is invalid or it has expired"
     end
   end
 end
diff --git a/priv/templates/phx.gen.auth/forgot_password_live_test.exs b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
deleted file mode 100644
index 2a7f5b362..000000000
--- a/priv/templates/phx.gen.auth/forgot_password_live_test.exs
+++ /dev/null
@@ -1,63 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPasswordTest do
-  use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
-
-  import Phoenix.LiveViewTest
-  import <%= inspect context.module %>Fixtures
-
-  alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %><%= schema.repo_alias %>
-
-  describe "Forgot password page" do
-    test "renders email page", %{conn: conn} do
-      {:ok, lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
-
-      assert html =~ "Forgot your password?"
-      assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/register"}"]|, "Register")
-      assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/log-in"}"]|, "Log in")
-    end
-
-    test "redirects if already logged in", %{conn: conn} do
-      result =
-        conn
-        |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
-        |> live(~p"<%= schema.route_prefix %>/reset-password")
-        |> follow_redirect(conn, ~p"/")
-
-      assert {:ok, _conn} = result
-    end
-  end
-
-  describe "Reset link" do
-    setup do
-      %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-    end
-
-    test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
-
-      {:ok, conn} =
-        lv
-        |> form("#reset_password_form", <%= schema.singular %>: %{"email" => <%= schema.singular %>.email})
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
-
-      assert Repo.get_by!(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context ==
-               "reset_password"
-    end
-
-    test "does not send reset password token if email is invalid", %{conn: conn} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
-
-      {:ok, conn} =
-        lv
-        |> form("#reset_password_form", <%= schema.singular %>: %{"email" => "unknown@example.com"})
-        |> render_submit()
-        |> follow_redirect(conn, ~p"/")
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/login_live_test.exs b/priv/templates/phx.gen.auth/login_live_test.exs
index 11516b78a..5b6d90e69 100644
--- a/priv/templates/phx.gen.auth/login_live_test.exs
+++ b/priv/templates/phx.gen.auth/login_live_test.exs
@@ -4,56 +4,76 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   import Phoenix.LiveViewTest
   import <%= inspect context.module %>Fixtures
 
-  describe "Log in page" do
-    test "renders log in page", %{conn: conn} do
+  describe "login page" do
+    test "renders login page", %{conn: conn} do
       {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       assert html =~ "Log in"
       assert html =~ "Register"
-      assert html =~ "Forgot your password?"
+      assert html =~ "Log in with email"
     end
+  end
+
+  describe "<%= schema.singular %> login - magic link" do
+    test "sends magic link email when <%= schema.singular %> exists", %{conn: conn} do
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      {:ok, _lv, html} =
+        form(lv, "#login_form_magic", <%= schema.singular %>: %{email: <%= schema.singular %>.email})
+        |> render_submit()
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
-    test "redirects if already logged in", %{conn: conn} do
-      result =
-        conn
-        |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
-        |> live(~p"<%= schema.route_prefix %>/log-in")
-        |> follow_redirect(conn, ~p"/")
+      assert html =~ "If your email is in our system"
 
-      assert {:ok, _conn} = result
+      assert <%= inspect schema.repo %>.get_by!(<%= inspect context.module %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context ==
+               "login"
+    end
+
+    test "does not disclose if <%= schema.singular %> is registered", %{conn: conn} do
+      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      {:ok, _lv, html} =
+        form(lv, "#login_form_magic", <%= schema.singular %>: %{email: "idonotexist@example.com"})
+        |> render_submit()
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      assert html =~ "If your email is in our system"
     end
   end
 
-  describe "<%= schema.singular %> login" do
-    test "redirects if <%= schema.singular %> login with valid credentials", %{conn: conn} do
-      password = "123456789abcd"
-      <%= schema.singular %> = <%= schema.singular %>_fixture(%{password: password})
+  describe "<%= schema.singular %> login - password" do
+    test "redirects if <%= schema.singular %> logs in with valid credentials", %{conn: conn} do
+      <%= schema.singular %> = <%= schema.singular %>_fixture() |> set_password()
 
       {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       form =
-        form(lv, "#login_form", <%= schema.singular %>: %{email: <%= schema.singular %>.email, password: password, remember_me: true})
+        form(lv, "#login_form_password",
+          <%= schema.singular %>: %{email: <%= schema.singular %>.email, password: valid_<%= schema.singular %>_password(), remember_me: true}
+        )
 
       conn = submit_form(form, conn)
 
       assert redirected_to(conn) == ~p"/"
     end
 
-    test "redirects to login page with a flash error if there are no valid credentials", %{
+    test "redirects to login page with a flash error if credentials are invalid", %{
       conn: conn
     } do
       {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
 
       form =
-        form(lv, "#login_form",
+        form(lv, "#login_form_password",
           <%= schema.singular %>: %{email: "test@email.com", password: "123456", remember_me: true}
         )
 
-      conn = submit_form(form, conn)
+      render_submit(form)
 
+      conn = follow_trigger_action(form, conn)
       assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
-
-      assert redirected_to(conn) == "<%= schema.route_prefix %>/log-in"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
     end
   end
 
@@ -69,19 +89,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert login_html =~ "Register"
     end
+  end
 
-    test "redirects to forgot password page when the Forgot Password button is clicked", %{
-      conn: conn
-    } do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
+  describe "re-authentication (sudo mode)" do
+    setup %{conn: conn} do
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      %{<%= schema.singular %>: <%= schema.singular %>, conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>)}
+    end
 
-      {:ok, conn} =
-        lv
-        |> element(~s|main a:fl-contains("Forgot your password?")|)
-        |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/reset-password")
+    test "shows login page with email filled in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      assert html =~ "You need to reauthenticate"
+      refute html =~ "Register"
+      assert html =~ "Log in with email"
 
-      assert conn.resp_body =~ "Forgot your password?"
+      assert html =~
+               ~s(<input type="email" name="<%= schema.singular %>[email]" id="login_form_magic_email" value="#{<%= schema.singular %>.email}")
     end
   end
 end
diff --git a/priv/templates/phx.gen.auth/registration_controller_test.exs b/priv/templates/phx.gen.auth/registration_controller_test.exs
index 53dfbb2e6..62db2c628 100644
--- a/priv/templates/phx.gen.auth/registration_controller_test.exs
+++ b/priv/templates/phx.gen.auth/registration_controller_test.exs
@@ -21,7 +21,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   describe "POST <%= schema.route_prefix %>/register" do
     @tag :capture_log
-    test "creates account and logs the <%= schema.singular %> in", %{conn: conn} do
+    test "creates account but does not log in", %{conn: conn} do
       email = unique_<%= schema.singular %>_email()
 
       conn =
@@ -29,27 +29,22 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           "<%= schema.singular %>" => valid_<%= schema.singular %>_attributes(email: email)
         })
 
-      assert get_session(conn, :<%= schema.singular %>_token)
-      assert redirected_to(conn) == ~p"/"
+      refute get_session(conn, :<%= schema.singular %>_token)
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
 
-      # Now do a logged in request and assert on the menu
-      conn = get(conn, ~p"/")
-      response = html_response(conn, 200)
-      assert response =~ email
-      assert response =~ ~p"<%= schema.route_prefix %>/settings"
-      assert response =~ ~p"<%= schema.route_prefix %>/log-out"
+      assert conn.assigns.flash["info"] =~
+               ~r/An email was sent to .*, please access it to confirm your account/
     end
 
     test "render errors for invalid data", %{conn: conn} do
       conn =
         post(conn, ~p"<%= schema.route_prefix %>/register", %{
-          "<%= schema.singular %>" => %{"email" => "with spaces", "password" => "too short"}
+          "<%= schema.singular %>" => %{"email" => "with spaces"}
         })
 
       response = html_response(conn, 200)
       assert response =~ "Register"
       assert response =~ "must have the @ sign and no spaces"
-      assert response =~ "should be at least 12 character"
     end
   end
 end
diff --git a/priv/templates/phx.gen.auth/registration_live_test.exs b/priv/templates/phx.gen.auth/registration_live_test.exs
index 8c878d301..0c98f2033 100644
--- a/priv/templates/phx.gen.auth/registration_live_test.exs
+++ b/priv/templates/phx.gen.auth/registration_live_test.exs
@@ -28,31 +28,26 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         lv
         |> element("#registration_form")
-        |> render_change(<%= schema.singular %>: %{"email" => "with spaces", "password" => "too short"})
+        |> render_change(<%= schema.singular %>: %{"email" => "with spaces"})
 
       assert result =~ "Register"
       assert result =~ "must have the @ sign and no spaces"
-      assert result =~ "should be at least 12 character"
     end
   end
 
   describe "register <%= schema.singular %>" do
-    test "creates account and logs the <%= schema.singular %> in", %{conn: conn} do
+    test "creates account but does not log in", %{conn: conn} do
       {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/register")
 
       email = unique_<%= schema.singular %>_email()
       form = form(lv, "#registration_form", <%= schema.singular %>: valid_<%= schema.singular %>_attributes(email: email))
-      render_submit(form)
-      conn = follow_trigger_action(form, conn)
 
-      assert redirected_to(conn) == ~p"/"
+      {:ok, _lv, html} =
+        render_submit(form)
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
 
-      # Now do a logged in request and assert on the menu
-      conn = get(conn, ~p"/")
-      response = html_response(conn, 200)
-      assert response =~ email
-      assert response =~ "Settings"
-      assert response =~ "Log out"
+      assert html =~
+               ~r/An email was sent to .*, please access it to confirm your account/
     end
 
     test "renders errors for duplicated email", %{conn: conn} do
@@ -63,7 +58,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         lv
         |> form("#registration_form",
-          <%= schema.singular %>: %{"email" => <%= schema.singular %>.email, "password" => "valid_password"}
+          <%= schema.singular %>: %{"email" => <%= schema.singular %>.email}
         )
         |> render_submit()
 
diff --git a/priv/templates/phx.gen.auth/reset_password_controller_test.exs b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
deleted file mode 100644
index dcf0dc5c9..000000000
--- a/priv/templates/phx.gen.auth/reset_password_controller_test.exs
+++ /dev/null
@@ -1,123 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordControllerTest do
-  use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
-
-  alias <%= inspect context.module %>
-  alias <%= inspect schema.repo %><%= schema.repo_alias %>
-  import <%= inspect context.module %>Fixtures
-
-  setup do
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-  end
-
-  describe "GET <%= schema.route_prefix %>/reset-password" do
-    test "renders the reset password page", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password")
-      response = html_response(conn, 200)
-      assert response =~ "Forgot your password?"
-    end
-  end
-
-  describe "POST <%= schema.route_prefix %>/reset-password" do
-    @tag :capture_log
-    test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn =
-        post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
-          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
-        })
-
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.get_by!(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context == "reset_password"
-    end
-
-    test "does not send reset password token if email is invalid", %{conn: conn} do
-      conn =
-        post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
-          "<%= schema.singular %>" => %{"email" => "unknown@example.com"}
-        })
-
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "If your email is in our system"
-
-      assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
-    end
-  end
-
-  describe "GET <%= schema.route_prefix %>/reset-password/:token" do
-    setup %{<%= schema.singular %>: <%= schema.singular %>} do
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, url)
-        end)
-
-      %{token: token}
-    end
-
-    test "renders reset password", %{conn: conn, token: token} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-      assert html_response(conn, 200) =~ "Reset password"
-    end
-
-    test "does not render reset password with invalid token", %{conn: conn} do
-      conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "Reset password link is invalid or it has expired"
-    end
-  end
-
-  describe "PUT <%= schema.route_prefix %>/reset-password/:token" do
-    setup %{<%= schema.singular %>: <%= schema.singular %>} do
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, url)
-        end)
-
-      %{token: token}
-    end
-
-    test "resets password once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token} do
-      conn =
-        put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
-          "<%= schema.singular %>" => %{
-            "password" => "new valid password",
-            "password_confirmation" => "new valid password"
-          }
-        })
-
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
-      refute get_session(conn, :<%= schema.singular %>_token)
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
-               "Password reset successfully"
-
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
-    end
-
-    test "does not reset password on invalid data", %{conn: conn, token: token} do
-      conn =
-        put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
-          "<%= schema.singular %>" => %{
-            "password" => "too short",
-            "password_confirmation" => "does not match"
-          }
-        })
-
-      assert html_response(conn, 200) =~ "something went wrong"
-    end
-
-    test "does not reset password with invalid token", %{conn: conn} do
-      conn = put(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
-      assert redirected_to(conn) == ~p"/"
-
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
-               "Reset password link is invalid or it has expired"
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/reset_password_live_test.exs b/priv/templates/phx.gen.auth/reset_password_live_test.exs
deleted file mode 100644
index 6e73bfe8e..000000000
--- a/priv/templates/phx.gen.auth/reset_password_live_test.exs
+++ /dev/null
@@ -1,118 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPasswordTest do
-  use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
-
-  import Phoenix.LiveViewTest
-  import <%= inspect context.module %>Fixtures
-
-  alias <%= inspect context.module %>
-
-  setup do
-    <%= schema.singular %> = <%= schema.singular %>_fixture()
-
-    token =
-      extract_<%= schema.singular %>_token(fn url ->
-        <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, url)
-      end)
-
-    %{token: token, <%= schema.singular %>: <%= schema.singular %>}
-  end
-
-  describe "Reset password page" do
-    test "renders reset password with valid token", %{conn: conn, token: token} do
-      {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      assert html =~ "Reset Password"
-    end
-
-    test "does not render reset password with invalid token", %{conn: conn} do
-      {:error, {:redirect, to}} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/invalid")
-
-      assert to == %{
-               flash: %{"error" => "Reset password link is invalid or it has expired."},
-               to: ~p"/"
-             }
-    end
-
-    test "renders errors for invalid data", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      result =
-        lv
-        |> element("#reset_password_form")
-        |> render_change(
-          <%= schema.singular %>: %{"password" => "secret12", "password_confirmation" => "secret123456"}
-        )
-
-      assert result =~ "should be at least 12 character"
-      assert result =~ "does not match password"
-    end
-  end
-
-  describe "Reset Password" do
-    test "resets password once", %{conn: conn, token: token, <%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      {:ok, conn} =
-        lv
-        |> form("#reset_password_form",
-          <%= schema.singular %>: %{
-            "password" => "new valid password",
-            "password_confirmation" => "new valid password"
-          }
-        )
-        |> render_submit()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
-
-      refute get_session(conn, :<%= schema.singular %>_token)
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
-    end
-
-    test "does not reset password on invalid data", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      result =
-        lv
-        |> form("#reset_password_form",
-          <%= schema.singular %>: %{
-            "password" => "too short",
-            "password_confirmation" => "does not match"
-          }
-        )
-        |> render_submit()
-
-      assert result =~ "Reset Password"
-      assert result =~ "should be at least 12 character(s)"
-      assert result =~ "does not match password"
-    end
-  end
-
-  describe "Reset password navigation" do
-    test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      {:ok, conn} =
-        lv
-        |> element(~s|main a:fl-contains("Log in")|)
-        |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
-
-      assert conn.resp_body =~ "Log in"
-    end
-
-    test "redirects to registration page when the Register button is clicked", %{
-      conn: conn,
-      token: token
-    } do
-      {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
-
-      {:ok, conn} =
-        lv
-        |> element(~s|main a:fl-contains("Register")|)
-        |> render_click()
-        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/register")
-
-      assert conn.resp_body =~ "Register"
-    end
-  end
-end
diff --git a/priv/templates/phx.gen.auth/session_controller_test.exs b/priv/templates/phx.gen.auth/session_controller_test.exs
index 8004208b1..6c2175de2 100644
--- a/priv/templates/phx.gen.auth/session_controller_test.exs
+++ b/priv/templates/phx.gen.auth/session_controller_test.exs
@@ -2,28 +2,81 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
 
   import <%= inspect context.module %>Fixtures
+  alias <%= inspect context.module %>
 
   setup do
-    %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
+    %{unconfirmed_<%= schema.singular %>: unconfirmed_<%= schema.singular %>_fixture(), <%= schema.singular %>: <%= schema.singular %>_fixture()}
   end<%= if not live? do %>
 
   describe "GET <%= schema.route_prefix %>/log-in" do
-    test "renders log in page", %{conn: conn} do
+    test "renders login page", %{conn: conn} do
       conn = get(conn, ~p"<%= schema.route_prefix %>/log-in")
       response = html_response(conn, 200)
       assert response =~ "Log in"
       assert response =~ ~p"<%= schema.route_prefix %>/register"
-      assert response =~ "Forgot your password?"
+      assert response =~ "Log in with email"
     end
 
-    test "redirects if already logged in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
-      conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> get(~p"<%= schema.route_prefix %>/log-in")
-      assert redirected_to(conn) == ~p"/"
+    test "renders login page with email filled in (sudo mode)", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      html =
+        conn
+        |> log_in_<%= schema.singular %>(<%= schema.singular %>)
+        |> get(~p"<%= schema.route_prefix %>/log-in")
+        |> html_response(200)
+
+      assert html =~ "You need to reauthenticate"
+      refute html =~ "Register"
+      assert html =~ "Log in with email"
+
+      assert html =~
+               ~s(<input type="email" name="<%= schema.singular %>[email]" id="login_form_magic_email" value="#{<%= schema.singular %>.email}")
+    end
+
+    test "renders login page (email + password)", %{conn: conn} do
+      conn = get(conn, ~p"<%= schema.route_prefix %>/log-in?mode=password")
+      response = html_response(conn, 200)
+      assert response =~ "Log in"
+      assert response =~ ~p"<%= schema.route_prefix %>/register"
+      assert response =~ "Log in with email"
+    end
+  end
+
+  describe "GET <%= schema.route_prefix %>/log-in/:token" do
+    test "renders confirmation page for unconfirmed <%= schema.singular %>", %{conn: conn, unconfirmed_<%= schema.singular %>: <%= schema.singular %>} do
+      token =
+        extract_<%= schema.singular %>_token(fn url ->
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+        end)
+
+      conn = get(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+      assert html_response(conn, 200) =~ "Confirm my account"
+    end
+
+    test "renders login page for confirmed <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      token =
+        extract_<%= schema.singular %>_token(fn url ->
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
+        end)
+
+      conn = get(conn, ~p"<%= schema.route_prefix %>/log-in/#{token}")
+      html = html_response(conn, 200)
+      refute html =~ "Confirm my account"
+      assert html =~ "Log in"
+    end
+
+    test "raises error for invalid token", %{conn: conn} do
+      conn = get(conn, ~p"<%= schema.route_prefix %>/log-in/invalid-token")
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
+
+      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+               "Magic link is invalid or it has expired."
     end
   end<% end %>
 
-  describe "POST <%= schema.route_prefix %>/log-in" do
+  describe "POST <%= schema.route_prefix %>/log-in - email and password" do
     test "logs the <%= schema.singular %> in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      <%= schema.singular %> = set_password(<%= schema.singular %>)
+
       conn =
         post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => valid_<%= schema.singular %>_password()}
@@ -41,6 +94,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "logs the <%= schema.singular %> in with remember me", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      <%= schema.singular %> = set_password(<%= schema.singular %>)
+
       conn =
         post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
           "<%= schema.singular %>" => %{
@@ -55,6 +110,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     end
 
     test "logs the <%= schema.singular %> in with return to", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      <%= schema.singular %> = set_password(<%= schema.singular %>)
+
       conn =
         conn
         |> init_test_session(<%= schema.singular %>_return_to: "/foo/bar")
@@ -67,58 +124,86 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert redirected_to(conn) == "/foo/bar"
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
-    end<%= if live? do %>
+    end
 
-    test "login following registration", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+    test "<%= if live?, do: "redirects to login page", else: "emits error message" %> with invalid credentials", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        conn
-        |> post(~p"<%= schema.route_prefix %>/log-in", %{
-          "_action" => "registered",
-          "<%= schema.singular %>" => %{
-            "email" => <%= schema.singular %>.email,
-            "password" => valid_<%= schema.singular %>_password()
-          }
+        post(conn, ~p"<%= schema.route_prefix %>/log-in?mode=password", %{
+          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => "invalid_password"}
         })
 
-      assert redirected_to(conn) == ~p"/"
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Account created successfully"
+      <%= if live? do %>assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"<% else %>response = html_response(conn, 200)
+      assert response =~ "Log in"
+      assert response =~ "Invalid email or password"<% end %>
     end
+  end
 
-    test "login following password update", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+  describe "POST <%= schema.route_prefix %>/log-in - magic link" do
+    <%= if not live? do %>test "sends magic link email when <%= schema.singular %> exists", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
-        conn
-        |> post(~p"<%= schema.route_prefix %>/log-in", %{
-          "_action" => "password-updated",
-          "<%= schema.singular %>" => %{
-            "email" => <%= schema.singular %>.email,
-            "password" => valid_<%= schema.singular %>_password()
-          }
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
+          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
         })
 
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
-      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password updated successfully"
+      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
+      assert <%= inspect schema.repo %>.get_by!(<%= inspect context.alias %>.<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id).context == "login"
     end
 
-    test "redirects to login page with invalid credentials", %{conn: conn} do
+    <% end %>test "logs the <%= schema.singular %> in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {token, _hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
+
       conn =
         post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
-          "<%= schema.singular %>" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
+          "<%= schema.singular %>" => %{"token" => token}
         })
 
-      assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
-    end<% else %>
+      assert get_session(conn, :<%= schema.singular %>_token)
+      assert redirected_to(conn) == ~p"/"
+
+      # Now do a logged in request and assert on the menu
+      conn = get(conn, ~p"/")
+      response = html_response(conn, 200)
+      assert response =~ <%= schema.singular %>.email
+      assert response =~ ~p"<%= schema.route_prefix %>/settings"
+      assert response =~ ~p"<%= schema.route_prefix %>/log-out"
+    end
+
+    test "confirms unconfirmed <%= schema.singular %>", %{conn: conn, unconfirmed_<%= schema.singular %>: <%= schema.singular %>} do
+      {token, _hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
+      refute <%= schema.singular %>.confirmed_at
 
-    test "emits error message with invalid credentials", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       conn =
         post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
-          "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => "invalid_password"}
+          "<%= schema.singular %>" => %{"token" => token},
+          "_action" => "confirmed"
         })
 
+      assert get_session(conn, :<%= schema.singular %>_token)
+      assert redirected_to(conn) == ~p"/"
+      assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "<%= schema.human_singular %> confirmed successfully."
+
+      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).confirmed_at
+
+      # Now do a logged in request and assert on the menu
+      conn = get(conn, ~p"/")
       response = html_response(conn, 200)
-      assert response =~ "Log in"
-      assert response =~ "Invalid email or password"
-    end<% end %>
+      assert response =~ <%= schema.singular %>.email
+      assert response =~ ~p"<%= schema.route_prefix %>/settings"
+      assert response =~ ~p"<%= schema.route_prefix %>/log-out"
+    end
+
+    test "<%= if live?, do: "redirects to login page", else: "emits error message" %> when magic link is invalid", %{conn: conn} do
+      conn =
+        post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
+          "<%= schema.singular %>" => %{"token" => "invalid"}
+        })
+
+      <%= if live? do %>assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+               "The link is invalid or it has expired."
+
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"<% else %>assert html_response(conn, 200) =~ "The link is invalid or it has expired."<% end %>
+    end
   end
 
   describe "DELETE <%= schema.route_prefix %>/log-out" do
diff --git a/priv/templates/phx.gen.auth/settings_controller_test.exs b/priv/templates/phx.gen.auth/settings_controller_test.exs
index 6f5ce220d..c2705f144 100644
--- a/priv/templates/phx.gen.auth/settings_controller_test.exs
+++ b/priv/templates/phx.gen.auth/settings_controller_test.exs
@@ -18,6 +18,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn = get(conn, ~p"<%= schema.route_prefix %>/settings")
       assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
     end
+
+    @tag token_inserted_at: <%= inspect datetime_module %>.add(<%= inspect datetime_module %>.utc_now(), -11, :minute)
+    test "redirects if <%= schema.singular %> is not in sudo mode", %{conn: conn} do
+      conn = get(conn, ~p"<%= schema.route_prefix %>/settings")
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
+
+      assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
+               "You must re-authenticate to access this page."
+    end
   end
 
   describe "PUT <%= schema.route_prefix %>/settings (change password form)" do
@@ -25,7 +34,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       new_password_conn =
         put(conn, ~p"<%= schema.route_prefix %>/settings", %{
           "action" => "update_password",
-          "current_password" => valid_<%= schema.singular %>_password(),
           "<%= schema.singular %>" => %{
             "password" => "new valid password",
             "password_confirmation" => "new valid password"
@@ -46,7 +54,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       old_password_conn =
         put(conn, ~p"<%= schema.route_prefix %>/settings", %{
           "action" => "update_password",
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{
             "password" => "too short",
             "password_confirmation" => "does not match"
@@ -57,7 +64,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert response =~ "Settings"
       assert response =~ "should be at least 12 character(s)"
       assert response =~ "does not match password"
-      assert response =~ "is not valid"
 
       assert get_session(old_password_conn, :<%= schema.singular %>_token) == get_session(conn, :<%= schema.singular %>_token)
     end
@@ -69,7 +75,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn =
         put(conn, ~p"<%= schema.route_prefix %>/settings", %{
           "action" => "update_email",
-          "current_password" => valid_<%= schema.singular %>_password(),
           "<%= schema.singular %>" => %{"email" => unique_<%= schema.singular %>_email()}
         })
 
@@ -85,14 +90,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       conn =
         put(conn, ~p"<%= schema.route_prefix %>/settings", %{
           "action" => "update_email",
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{"email" => "with spaces"}
         })
 
       response = html_response(conn, 200)
       assert response =~ "Settings"
       assert response =~ "must have the @ sign and no spaces"
-      assert response =~ "is not valid"
     end
   end
 
diff --git a/priv/templates/phx.gen.auth/settings_live_test.exs b/priv/templates/phx.gen.auth/settings_live_test.exs
index 32f4ebd92..6642b2c94 100644
--- a/priv/templates/phx.gen.auth/settings_live_test.exs
+++ b/priv/templates/phx.gen.auth/settings_live_test.exs
@@ -13,7 +13,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         |> live(~p"<%= schema.route_prefix %>/settings")
 
       assert html =~ "Change Email"
-      assert html =~ "Change Password"
+      assert html =~ "Save Password"
     end
 
     test "redirects if <%= schema.singular %> is not logged in", %{conn: conn} do
@@ -23,16 +23,27 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert path == ~p"<%= schema.route_prefix %>/log-in"
       assert %{"error" => "You must log in to access this page."} = flash
     end
+
+    test "redirects if <%= schema.singular %> is not in sudo mode", %{conn: conn} do
+      {:ok, conn} =
+        conn
+        |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture(),
+          token_inserted_at: <%= inspect datetime_module %>.add(<%= inspect datetime_module %>.utc_now(), -11, :minute)
+        )
+        |> live(~p"<%= schema.route_prefix %>/settings")
+        |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
+
+      assert conn.resp_body =~ "You must re-authenticate to access this page."
+    end
   end
 
   describe "update email form" do
     setup %{conn: conn} do
-      password = valid_<%= schema.singular %>_password()
-      <%= schema.singular %> = <%= schema.singular %>_fixture(%{password: password})
-      %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>), <%= schema.singular %>: <%= schema.singular %>, password: password}
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>), <%= schema.singular %>: <%= schema.singular %>}
     end
 
-    test "updates the <%= schema.singular %> email", %{conn: conn, password: password, <%= schema.singular %>: <%= schema.singular %>} do
+    test "updates the <%= schema.singular %> email", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       new_email = unique_<%= schema.singular %>_email()
 
       {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/settings")
@@ -40,7 +51,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         lv
         |> form("#email_form", %{
-          "current_password" => password,
           "<%= schema.singular %>" => %{"email" => new_email}
         })
         |> render_submit()
@@ -57,7 +67,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         |> element("#email_form")
         |> render_change(%{
           "action" => "update_email",
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{"email" => "with spaces"}
         })
 
@@ -71,32 +80,28 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         lv
         |> form("#email_form", %{
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
         })
         |> render_submit()
 
       assert result =~ "Change Email"
       assert result =~ "did not change"
-      assert result =~ "is not valid"
     end
   end
 
   describe "update password form" do
     setup %{conn: conn} do
-      password = valid_<%= schema.singular %>_password()
-      <%= schema.singular %> = <%= schema.singular %>_fixture(%{password: password})
-      %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>), <%= schema.singular %>: <%= schema.singular %>, password: password}
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      %{conn: log_in_<%= schema.singular %>(conn, <%= schema.singular %>), <%= schema.singular %>: <%= schema.singular %>}
     end
 
-    test "updates the <%= schema.singular %> password", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, password: password} do
+    test "updates the <%= schema.singular %> password", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       new_password = valid_<%= schema.singular %>_password()
 
       {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/settings")
 
       form =
         form(lv, "#password_form", %{
-          "current_password" => password,
           "<%= schema.singular %>" => %{
             "email" => <%= schema.singular %>.email,
             "password" => new_password,
@@ -125,14 +130,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> element("#password_form")
         |> render_change(%{
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{
             "password" => "too short",
             "password_confirmation" => "does not match"
           }
         })
 
-      assert result =~ "Change Password"
+      assert result =~ "Save Password"
       assert result =~ "should be at least 12 character(s)"
       assert result =~ "does not match password"
     end
@@ -143,7 +147,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       result =
         lv
         |> form("#password_form", %{
-          "current_password" => "invalid",
           "<%= schema.singular %>" => %{
             "password" => "too short",
             "password_confirmation" => "does not match"
@@ -151,10 +154,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         })
         |> render_submit()
 
-      assert result =~ "Change Password"
+      assert result =~ "Save Password"
       assert result =~ "should be at least 12 character(s)"
       assert result =~ "does not match password"
-      assert result =~ "is not valid"
     end
   end
 
diff --git a/priv/templates/phx.gen.auth/test_cases.exs b/priv/templates/phx.gen.auth/test_cases.exs
index 910f46629..98632c46a 100644
--- a/priv/templates/phx.gen.auth/test_cases.exs
+++ b/priv/templates/phx.gen.auth/test_cases.exs
@@ -18,12 +18,12 @@
     end
 
     test "does not return the <%= schema.singular %> if the password is not valid" do
-      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      <%= schema.singular %> = <%= schema.singular %>_fixture() |> set_password()
       refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "invalid")
     end
 
     test "returns the <%= schema.singular %> if the email and password are valid" do
-      %{id: id} = <%= schema.singular %> = <%= schema.singular %>_fixture()
+      %{id: id} = <%= schema.singular %> = <%= schema.singular %>_fixture() |> set_password()
 
       assert %<%= inspect schema.alias %>{id: ^id} =
                <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, valid_<%= schema.singular %>_password())
@@ -39,34 +39,27 @@
 
     test "returns the <%= schema.singular %> with the given id" do
       %{id: id} = <%= schema.singular %> = <%= schema.singular %>_fixture()
-      assert %<%= inspect schema.alias %>{id: ^id} = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id)
+      assert %<%= inspect schema.alias %>{id: ^id} = <%= inspect context.alias %>.get_<%= schema.singular %>!(<%=schema.singular %>.id)
     end
   end
 
   describe "register_<%= schema.singular %>/1" do
-    test "requires email and password to be set" do
+    test "requires email to be set" do
       {:error, changeset} = <%= inspect context.alias %>.register_<%= schema.singular %>(%{})
 
-      assert %{
-               password: ["can't be blank"],
-               email: ["can't be blank"]
-             } = errors_on(changeset)
+      assert %{email: ["can't be blank"]} = errors_on(changeset)
     end
 
-    test "validates email and password when given" do
-      {:error, changeset} = <%= inspect context.alias %>.register_<%= schema.singular %>(%{email: "not valid", password: "not valid"})
+    test "validates email when given" do
+      {:error, changeset} = <%= inspect context.alias %>.register_<%= schema.singular %>(%{email: "not valid"})
 
-      assert %{
-               email: ["must have the @ sign and no spaces"],
-               password: ["should be at least 12 character(s)"]
-             } = errors_on(changeset)
+      assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
     end
 
-    test "validates maximum values for email and password for security" do
+    test "validates maximum values for email for security" do
       too_long = String.duplicate("db", 100)
-      {:error, changeset} = <%= inspect context.alias %>.register_<%= schema.singular %>(%{email: too_long, password: too_long})
+      {:error, changeset} = <%= inspect context.alias %>.register_<%= schema.singular %>(%{email: too_long})
       assert "should be at most 160 character(s)" in errors_on(changeset).email
-      assert "should be at most 72 character(s)" in errors_on(changeset).password
     end
 
     test "validates email uniqueness" do
@@ -79,96 +72,42 @@
       assert "has already been taken" in errors_on(changeset).email
     end
 
-    test "registers <%= schema.plural %> with a hashed password" do
+    test "registers <%= schema.plural %> without password" do
       email = unique_<%= schema.singular %>_email()
       {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.register_<%= schema.singular %>(valid_<%= schema.singular %>_attributes(email: email))
       assert <%= schema.singular %>.email == email
-      assert is_binary(<%= schema.singular %>.hashed_password)
+      assert is_nil(<%= schema.singular %>.hashed_password)
       assert is_nil(<%= schema.singular %>.confirmed_at)
       assert is_nil(<%= schema.singular %>.password)
     end
   end
 
-  describe "change_<%= schema.singular %>_registration/2" do
-    test "returns a changeset" do
-      assert %Ecto.Changeset{} = changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{})
-      assert changeset.required == [:password, :email]
-    end
+  describe "sudo_mode?/2" do
+    test "validates the authenticated_at time" do
+      now = <%= inspect datetime_module %>.utc_now()
 
-    test "allows fields to be set" do
-      email = unique_<%= schema.singular %>_email()
-      password = valid_<%= schema.singular %>_password()
+      assert <%= inspect context.alias %>.sudo_mode?(%<%= inspect schema.alias %>{authenticated_at: <%= inspect datetime_module %>.utc_now()})
+      assert <%= inspect context.alias %>.sudo_mode?(%<%= inspect schema.alias %>{authenticated_at: <%= inspect datetime_module %>.add(now, -19, :minute)})
+      refute <%= inspect context.alias %>.sudo_mode?(%<%= inspect schema.alias %>{authenticated_at: <%= inspect datetime_module %>.add(now, -21, :minute)})
 
-      changeset =
-        <%= inspect context.alias %>.change_<%= schema.singular %>_registration(
-          %<%= inspect schema.alias %>{},
-          valid_<%= schema.singular %>_attributes(email: email, password: password)
-        )
+      # minute override
+      refute <%= inspect context.alias %>.sudo_mode?(
+               %<%= inspect schema.alias %>{authenticated_at: <%= inspect datetime_module %>.add(now, -11, :minute)},
+               -10
+             )
 
-      assert changeset.valid?
-      assert get_change(changeset, :email) == email
-      assert get_change(changeset, :password) == password
-      assert is_nil(get_change(changeset, :hashed_password))
+      # not authenticated
+      refute <%= inspect context.alias %>.sudo_mode?(%<%= inspect schema.alias %>{})
     end
   end
 
-  describe "change_<%= schema.singular %>_email/2" do
+  describe "change_<%= schema.singular %>_email/3" do
     test "returns a <%= schema.singular %> changeset" do
       assert %Ecto.Changeset{} = changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(%<%= inspect schema.alias %>{})
       assert changeset.required == [:email]
     end
   end
 
-  describe "apply_<%= schema.singular %>_email/3" do
-    setup do
-      %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-    end
-
-    test "requires email to change", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:error, changeset} = <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{})
-      assert %{email: ["did not change"]} = errors_on(changeset)
-    end
-
-    test "validates email", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:error, changeset} =
-        <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{email: "not valid"})
-
-      assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
-    end
-
-    test "validates maximum value for email for security", %{<%= schema.singular %>: <%= schema.singular %>} do
-      too_long = String.duplicate("db", 100)
-
-      {:error, changeset} =
-        <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{email: too_long})
-
-      assert "should be at most 160 character(s)" in errors_on(changeset).email
-    end
-
-    test "validates email uniqueness", %{<%= schema.singular %>: <%= schema.singular %>} do
-      %{email: email} = <%= schema.singular %>_fixture()
-      password = valid_<%= schema.singular %>_password()
-
-      {:error, changeset} = <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, password, %{email: email})
-
-      assert "has already been taken" in errors_on(changeset).email
-    end
-
-    test "validates current password", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:error, changeset} =
-        <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, "invalid", %{email: unique_<%= schema.singular %>_email()})
-
-      assert %{current_password: ["is not valid"]} = errors_on(changeset)
-    end
-
-    test "applies the email without persisting it", %{<%= schema.singular %>: <%= schema.singular %>} do
-      email = unique_<%= schema.singular %>_email()
-      {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.apply_<%= schema.singular %>_email(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{email: email})
-      assert <%= schema.singular %>.email == email
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.id).email != email
-    end
-  end
-
   describe "deliver_<%= schema.singular %>_update_email_instructions/3" do
     setup do
       %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
@@ -190,7 +129,7 @@
 
   describe "update_<%= schema.singular %>_email/2" do
     setup do
-      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      <%= schema.singular %> = unconfirmed_<%= schema.singular %>_fixture()
       email = unique_<%= schema.singular %>_email()
 
       token =
@@ -206,8 +145,6 @@
       changed_<%= schema.singular %> = Repo.get!(<%= inspect schema.alias %>, <%= schema.singular %>.id)
       assert changed_<%= schema.singular %>.email != <%= schema.singular %>.email
       assert changed_<%= schema.singular %>.email == email
-      assert changed_<%= schema.singular %>.confirmed_at
-      assert changed_<%= schema.singular %>.confirmed_at != <%= schema.singular %>.confirmed_at
       refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
     end
 
@@ -231,7 +168,7 @@
     end
   end
 
-  describe "change_<%= schema.singular %>_password/2" do
+  describe "change_<%= schema.singular %>_password/3" do
     test "returns a <%= schema.singular %> changeset" do
       assert %Ecto.Changeset{} = changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(%<%= inspect schema.alias %>{})
       assert changeset.required == [:password]
@@ -239,9 +176,13 @@
 
     test "allows fields to be set" do
       changeset =
-        <%= inspect context.alias %>.change_<%= schema.singular %>_password(%<%= inspect schema.alias %>{}, %{
-          "password" => "new valid password"
-        })
+        <%= inspect context.alias %>.change_<%= schema.singular %>_password(
+          %<%= inspect schema.alias %>{},
+          %{
+            "password" => "new valid password"
+          },
+          hash_password: false
+        )
 
       assert changeset.valid?
       assert get_change(changeset, :password) == "new valid password"
@@ -249,14 +190,14 @@
     end
   end
 
-  describe "update_<%= schema.singular %>_password/3" do
+  describe "update_<%= schema.singular %>_password/2" do
     setup do
       %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
     end
 
     test "validates password", %{<%= schema.singular %>: <%= schema.singular %>} do
       {:error, changeset} =
-        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{
+        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, %{
           password: "not valid",
           password_confirmation: "another"
         })
@@ -271,24 +212,18 @@
       too_long = String.duplicate("db", 100)
 
       {:error, changeset} =
-        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{password: too_long})
+        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, %{password: too_long})
 
       assert "should be at most 72 character(s)" in errors_on(changeset).password
     end
 
-    test "validates current password", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:error, changeset} =
-        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, "invalid", %{password: valid_<%= schema.singular %>_password()})
-
-      assert %{current_password: ["is not valid"]} = errors_on(changeset)
-    end
-
     test "updates the password", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, <%= schema.singular %>} =
-        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{
+      {:ok, <%= schema.singular %>, expired_tokens} =
+        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, %{
           password: "new valid password"
         })
 
+      assert expired_tokens == []
       assert is_nil(<%= schema.singular %>.password)
       assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
     end
@@ -296,8 +231,8 @@
     test "deletes all tokens for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
       _ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
 
-      {:ok, _} =
-        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{
+      {:ok, _, _} =
+        <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, %{
           password: "new valid password"
         })
 
@@ -348,150 +283,85 @@
     end
   end
 
-  describe "delete_<%= schema.singular %>_session_token/1" do
-    test "deletes the token" do
+  describe "get_<%= schema.singular %>_by_magic_link_token/1" do
+    setup do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
-      token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
-      assert <%= inspect context.alias %>.delete_<%= schema.singular %>_session_token(token) == :ok
-      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(token)
+      {encoded_token, _hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
+      %{<%= schema.singular %>: <%= schema.singular %>, token: encoded_token}
     end
-  end
 
-  describe "deliver_<%= schema.singular %>_confirmation_instructions/2" do
-    setup do
-      %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
+    test "returns <%= schema.singular %> by token", %{<%= schema.singular %>: <%= schema.singular %>, token: token} do
+      assert session_<%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_magic_link_token(token)
+      assert session_<%= schema.singular %>.id == <%= schema.singular %>.id
     end
 
-    test "sends token through notification", %{<%= schema.singular %>: <%= schema.singular %>} do
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, url)
-        end)
+    test "does not return <%= schema.singular %> for invalid token" do
+      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_magic_link_token("oops")
+    end
 
-      {:ok, token} = Base.url_decode64(token, padding: false)
-      assert <%= schema.singular %>_token = Repo.get_by(<%= inspect schema.alias %>Token, token: :crypto.hash(:sha256, token))
-      assert <%= schema.singular %>_token.<%= schema.singular %>_id == <%= schema.singular %>.id
-      assert <%= schema.singular %>_token.sent_to == <%= schema.singular %>.email
-      assert <%= schema.singular %>_token.context == "confirm"
+    test "does not return <%= schema.singular %> for expired token", %{token: token} do
+      {1, nil} = Repo.update_all(<%= inspect schema.alias %>Token, set: [inserted_at: ~N[2020-01-01 00:00:00]])
+      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_magic_link_token(token)
     end
   end
 
-  describe "confirm_<%= schema.singular %>/1" do
-    setup do
-      <%= schema.singular %> = <%= schema.singular %>_fixture()
+  describe "login_<%= schema.singular %>_by_magic_link/1" do
+    test "confirms <%= schema.singular %> and expires tokens" do
+      <%= schema.singular %> = unconfirmed_<%= schema.singular %>_fixture()
+      refute <%= schema.singular %>.confirmed_at
+      {encoded_token, hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
 
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, url)
-        end)
+      assert {:ok, <%= schema.singular %>, [%{token: ^hashed_token}]} =
+               <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(encoded_token)
 
-      %{<%= schema.singular %>: <%= schema.singular %>, token: token}
+      assert <%= schema.singular %>.confirmed_at
     end
 
-    test "confirms the email with a valid token", %{<%= schema.singular %>: <%= schema.singular %>, token: token} do
-      assert {:ok, confirmed_<%= schema.singular %>} = <%= inspect context.alias %>.confirm_<%= schema.singular %>(token)
-      assert confirmed_<%= schema.singular %>.confirmed_at
-      assert confirmed_<%= schema.singular %>.confirmed_at != <%= schema.singular %>.confirmed_at
-      assert Repo.get!(<%= inspect schema.alias %>, <%= schema.singular %>.id).confirmed_at
-      refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
+    test "returns <%= schema.singular %> and (deleted) token for confirmed <%= schema.singular %>" do
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      assert <%= schema.singular %>.confirmed_at
+      {encoded_token, _hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
+      assert {:ok, ^<%= schema.singular %>, []} = <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(encoded_token)
+      # one time use only
+      assert {:error, :not_found} = <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(encoded_token)
     end
 
-    test "does not confirm with invalid token", %{<%= schema.singular %>: <%= schema.singular %>} do
-      assert <%= inspect context.alias %>.confirm_<%= schema.singular %>("oops") == :error
-      refute Repo.get!(<%= inspect schema.alias %>, <%= schema.singular %>.id).confirmed_at
-      assert Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
+    test "raises when unconfirmed <%= schema.singular %> has password set" do
+      <%= schema.singular %> = unconfirmed_<%= schema.singular %>_fixture()
+      {1, nil} = Repo.update_all(<%= inspect schema.alias %>, set: [hashed_password: "hashed"])
+      {encoded_token, _hashed_token} = generate_<%= schema.singular %>_magic_link_token(<%= schema.singular %>)
+
+      assert_raise RuntimeError, ~r/magic link log in is not allowed/, fn ->
+        <%= inspect context.alias %>.login_<%= schema.singular %>_by_magic_link(encoded_token)
+      end
     end
+  end
 
-    test "does not confirm email if token expired", %{<%= schema.singular %>: <%= schema.singular %>, token: token} do
-      {1, nil} = Repo.update_all(<%= inspect schema.alias %>Token, set: [inserted_at: ~N[2020-01-01 00:00:00]])
-      assert <%= inspect context.alias %>.confirm_<%= schema.singular %>(token) == :error
-      refute Repo.get!(<%= inspect schema.alias %>, <%= schema.singular %>.id).confirmed_at
-      assert Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
+  describe "delete_<%= schema.singular %>_session_token/1" do
+    test "deletes the token" do
+      <%= schema.singular %> = <%= schema.singular %>_fixture()
+      token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
+      assert <%= inspect context.alias %>.delete_<%= schema.singular %>_session_token(token) == :ok
+      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(token)
     end
   end
 
-  describe "deliver_<%= schema.singular %>_reset_password_instructions/2" do
+  describe "deliver_login_instructions/2" do
     setup do
-      %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
+      %{<%= schema.singular %>: unconfirmed_<%= schema.singular %>_fixture()}
     end
 
     test "sends token through notification", %{<%= schema.singular %>: <%= schema.singular %>} do
       token =
         extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, url)
+          <%= inspect context.alias %>.deliver_login_instructions(<%= schema.singular %>, url)
         end)
 
       {:ok, token} = Base.url_decode64(token, padding: false)
       assert <%= schema.singular %>_token = Repo.get_by(<%= inspect schema.alias %>Token, token: :crypto.hash(:sha256, token))
       assert <%= schema.singular %>_token.<%= schema.singular %>_id == <%= schema.singular %>.id
       assert <%= schema.singular %>_token.sent_to == <%= schema.singular %>.email
-      assert <%= schema.singular %>_token.context == "reset_password"
-    end
-  end
-
-  describe "get_<%= schema.singular %>_by_reset_password_token/1" do
-    setup do
-      <%= schema.singular %> = <%= schema.singular %>_fixture()
-
-      token =
-        extract_<%= schema.singular %>_token(fn url ->
-          <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, url)
-        end)
-
-      %{<%= schema.singular %>: <%= schema.singular %>, token: token}
-    end
-
-    test "returns the <%= schema.singular %> with valid token", %{<%= schema.singular %>: %{id: id}, token: token} do
-      assert %<%= inspect schema.alias %>{id: ^id} = <%= inspect context.alias %>.get_<%= schema.singular %>_by_reset_password_token(token)
-      assert Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: id)
-    end
-
-    test "does not return the <%= schema.singular %> with invalid token", %{<%= schema.singular %>: <%= schema.singular %>} do
-      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_reset_password_token("oops")
-      assert Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
-    end
-
-    test "does not return the <%= schema.singular %> if token expired", %{<%= schema.singular %>: <%= schema.singular %>, token: token} do
-      {1, nil} = Repo.update_all(<%= inspect schema.alias %>Token, set: [inserted_at: ~N[2020-01-01 00:00:00]])
-      refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_reset_password_token(token)
-      assert Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
-    end
-  end
-
-  describe "reset_<%= schema.singular %>_password/2" do
-    setup do
-      %{<%= schema.singular %>: <%= schema.singular %>_fixture()}
-    end
-
-    test "validates password", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:error, changeset} =
-        <%= inspect context.alias %>.reset_<%= schema.singular %>_password(<%= schema.singular %>, %{
-          password: "not valid",
-          password_confirmation: "another"
-        })
-
-      assert %{
-               password: ["should be at least 12 character(s)"],
-               password_confirmation: ["does not match password"]
-             } = errors_on(changeset)
-    end
-
-    test "validates maximum values for password for security", %{<%= schema.singular %>: <%= schema.singular %>} do
-      too_long = String.duplicate("db", 100)
-      {:error, changeset} = <%= inspect context.alias %>.reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: too_long})
-      assert "should be at most 72 character(s)" in errors_on(changeset).password
-    end
-
-    test "updates the password", %{<%= schema.singular %>: <%= schema.singular %>} do
-      {:ok, updated_<%= schema.singular %>} = <%= inspect context.alias %>.reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "new valid password"})
-      assert is_nil(updated_<%= schema.singular %>.password)
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
-    end
-
-    test "deletes all tokens for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
-      _ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
-      {:ok, _} = <%= inspect context.alias %>.reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "new valid password"})
-      refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
+      assert <%= schema.singular %>_token.context == "login"
     end
   end
 
diff --git a/priv/templates/phx.gen.context/test_cases.exs b/priv/templates/phx.gen.context/test_cases.exs
index a6f9cfff9..edf38c916 100644
--- a/priv/templates/phx.gen.context/test_cases.exs
+++ b/priv/templates/phx.gen.context/test_cases.exs
@@ -13,7 +13,7 @@
 
     test "get_<%= schema.singular %>!/1 returns the <%= schema.singular %> with given id" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
-      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>) == <%= schema.singular %>
+      assert <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= primary_key %>) == <%= schema.singular %>
     end
 
     test "create_<%= schema.singular %>/1 with valid data creates a <%= schema.singular %>" do
@@ -38,13 +38,13 @@
     test "update_<%= schema.singular %>/2 with invalid data returns error changeset" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
       assert {:error, %Ecto.Changeset{}} = <%= inspect context.alias %>.update_<%= schema.singular %>(<%= schema.singular %>, @invalid_attrs)
-      assert <%= schema.singular %> == <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>)
+      assert <%= schema.singular %> == <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= primary_key %>)
     end
 
     test "delete_<%= schema.singular %>/1 deletes the <%= schema.singular %>" do
       <%= schema.singular %> = <%= schema.singular %>_fixture()
       assert {:ok, %<%= inspect schema.alias %>{}} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
-      assert_raise Ecto.NoResultsError, fn -> <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>) end
+      assert_raise Ecto.NoResultsError, fn -> <%= inspect context.alias %>.get_<%= schema.singular %>!(<%= schema.singular %>.<%= primary_key %>) end
     end
 
     test "change_<%= schema.singular %>/1 returns a <%= schema.singular %> changeset" do
diff --git a/priv/templates/phx.gen.html/controller_test.exs b/priv/templates/phx.gen.html/controller_test.exs
index 713234421..3472b8690 100644
--- a/priv/templates/phx.gen.html/controller_test.exs
+++ b/priv/templates/phx.gen.html/controller_test.exs
@@ -25,11 +25,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "redirects to show when data is valid", %{conn: conn} do
       conn = post(conn, ~p"<%= schema.route_prefix %>", <%= schema.singular %>: @create_attrs)
 
-      assert %{id: id} = redirected_params(conn)
-      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/#{id}"
+      assert %{<%= primary_key %>: <%= primary_key %>} = redirected_params(conn)
+      assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/#{<%= primary_key %>}"
 
-      conn = get(conn, ~p"<%= schema.route_prefix %>/#{id}")
-      assert html_response(conn, 200) =~ "<%= schema.human_singular %> #{id}"
+      conn = get(conn, ~p"<%= schema.route_prefix %>/#{<%= primary_key %>}")
+      assert html_response(conn, 200) =~ "<%= schema.human_singular %> #{<%= primary_key %>}"
     end
 
     test "renders errors when data is invalid", %{conn: conn} do
diff --git a/priv/templates/phx.gen.json/controller_test.exs b/priv/templates/phx.gen.json/controller_test.exs
index d6f3e4a3f..ec293f26f 100644
--- a/priv/templates/phx.gen.json/controller_test.exs
+++ b/priv/templates/phx.gen.json/controller_test.exs
@@ -27,12 +27,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   describe "create <%= schema.singular %>" do
     test "renders <%= schema.singular %> when data is valid", %{conn: conn} do
       conn = post(conn, ~p"<%= schema.api_route_prefix %>", <%= schema.singular %>: @create_attrs)
-      assert %{"id" => id} = json_response(conn, 201)["data"]
+      assert %{"<%= primary_key %>" => <%= primary_key %>} = json_response(conn, 201)["data"]
 
-      conn = get(conn, ~p"<%= schema.api_route_prefix %>/#{id}")
+      conn = get(conn, ~p"<%= schema.api_route_prefix %>/#{<%= primary_key %>}")
 
       assert %{
-               "id" => ^id<%= for {key, val} <- schema.params.create |> Phoenix.json_library().encode!() |> Phoenix.json_library().decode!() do %>,
+               "<%= primary_key %>" => ^<%= primary_key %><%= for {key, val} <- schema.params.create |> Phoenix.json_library().encode!() |> Phoenix.json_library().decode!() do %>,
                "<%= key %>" => <%= inspect(val) %><% end %>
              } = json_response(conn, 200)["data"]
     end
@@ -46,14 +46,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   describe "update <%= schema.singular %>" do
     setup [:create_<%= schema.singular %>]
 
-    test "renders <%= schema.singular %> when data is valid", %{conn: conn, <%= schema.singular %>: %<%= inspect schema.alias %>{id: id} = <%= schema.singular %>} do
+    test "renders <%= schema.singular %> when data is valid", %{conn: conn, <%= schema.singular %>: %<%= inspect schema.alias %>{<%= primary_key %>: <%= primary_key %>} = <%= schema.singular %>} do
       conn = put(conn, ~p"<%= schema.api_route_prefix %>/#{<%= schema.singular %>}", <%= schema.singular %>: @update_attrs)
-      assert %{"id" => ^id} = json_response(conn, 200)["data"]
+      assert %{"<%= primary_key %>" => ^<%= primary_key %>} = json_response(conn, 200)["data"]
 
-      conn = get(conn, ~p"<%= schema.api_route_prefix %>/#{id}")
+      conn = get(conn, ~p"<%= schema.api_route_prefix %>/#{<%= primary_key %>}")
 
       assert %{
-               "id" => ^id<%= for {key, val} <- schema.params.update |> Phoenix.json_library().encode!() |> Phoenix.json_library().decode!() do %>,
+               "<%= primary_key %>" => ^<%= primary_key %><%= for {key, val} <- schema.params.update |> Phoenix.json_library().encode!() |> Phoenix.json_library().decode!() do %>,
                "<%= key %>" => <%= inspect(val) %><% end %>
              } = json_response(conn, 200)["data"]
     end
diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs
index dd90d352d..82a06afcf 100644
--- a/priv/templates/phx.gen.live/live_test.exs
+++ b/priv/templates/phx.gen.live/live_test.exs
@@ -54,7 +54,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert {:ok, form_live, _html} =
                index_live
-               |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>} a", "Edit")
+               |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= primary_key %>} a", "Edit")
                |> render_click()
                |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
 
@@ -78,8 +78,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "deletes <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       {:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
 
-      assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>} a", "Delete") |> render_click()
-      refute has_element?(index_live, "#<%= schema.plural %>-#{<%= schema.singular %>.<%= schema.opts[:primary_key] || :id %>}")
+      assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.<%= primary_key %>} a", "Delete") |> render_click()
+      refute has_element?(index_live, "#<%= schema.plural %>-#{<%= schema.singular %>.<%= primary_key %>}")
     end
   end
 
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index fb90b40fb..ef3a39c40 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -86,7 +86,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
   test "generates with defaults (Prompt: --no-live)", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, false}
+      send(self(), {:mix_shell_input, :yes?, false})
 
       Gen.Auth.run(
         ~w(Accounts User users --no-compile),
@@ -109,7 +109,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ "Mailer.deliver(email)"
         assert file =~ ~s|from({"MyApp", "contact@example.com"})|
         assert file =~ ~s|deliver(user.email, "Confirmation instructions",|
-        assert file =~ ~s|deliver(user.email, "Reset password instructions",|
+        assert file =~ ~s|deliver(user.email, "Log in instructions",|
         assert file =~ ~s|deliver(user.email, "Update email instructions",|
       end)
 
@@ -117,18 +117,9 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       assert_file("test/support/fixtures/accounts_fixtures.ex")
       assert_file("lib/my_app_web/user_auth.ex")
       assert_file("test/my_app_web/user_auth_test.exs")
-      assert_file("lib/my_app_web/controllers/user_confirmation_html.ex")
-      assert_file("lib/my_app_web/controllers/user_confirmation_html/new.html.heex")
-      assert_file("lib/my_app_web/controllers/user_confirmation_controller.ex")
-      assert_file("test/my_app_web/controllers/user_confirmation_controller_test.exs")
       assert_file("lib/my_app_web/controllers/user_registration_controller.ex")
       assert_file("lib/my_app_web/controllers/user_registration_html.ex")
       assert_file("test/my_app_web/controllers/user_registration_controller_test.exs")
-      assert_file("lib/my_app_web/controllers/user_reset_password_controller.ex")
-      assert_file("lib/my_app_web/controllers/user_reset_password_html/edit.html.heex")
-      assert_file("lib/my_app_web/controllers/user_reset_password_html/new.html.heex")
-      assert_file("lib/my_app_web/controllers/user_reset_password_html.ex")
-      assert_file("test/my_app_web/controllers/user_reset_password_controller_test.exs")
       assert_file("lib/my_app_web/controllers/user_session_controller.ex")
       assert_file("lib/my_app_web/controllers/user_session_html/new.html.heex")
       assert_file("test/my_app_web/controllers/user_session_controller_test.exs")
@@ -161,12 +152,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log-in", UserSessionController, :new
-                   post "/users/log-in", UserSessionController, :create
-                   get "/users/reset-password", UserResetPasswordController, :new
-                   post "/users/reset-password", UserResetPasswordController, :create
-                   get "/users/reset-password/:token", UserResetPasswordController, :edit
-                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/", MyAppWeb do
@@ -180,11 +165,10 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
+                   get "/users/log-in", UserSessionController, :new
+                   get "/users/log-in/:token", UserSessionController, :confirm
+                   post "/users/log-in", UserSessionController, :create
                    delete "/users/log-out", UserSessionController, :delete
-                   get "/users/confirm", UserConfirmationController, :new
-                   post "/users/confirm", UserConfirmationController, :create
-                   get "/users/confirm/:token", UserConfirmationController, :edit
-                   post "/users/confirm/:token", UserConfirmationController, :update
                  end
                """
       end)
@@ -204,8 +188,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       end)
 
       assert_file("test/support/conn_case.ex", fn file ->
-        assert file =~ "def register_and_log_in_user(%{conn: conn})"
-        assert file =~ "def log_in_user(conn, user)"
+        assert file =~ "def register_and_log_in_user(%{conn: conn} = context)"
+        assert file =~ "def log_in_user(conn, user, opts \\\\ [])"
       end)
 
       assert_received {:mix_shell, :info,
@@ -222,7 +206,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
   test "generates with defaults (Prompt: --live)", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, true}
+      send(self(), {:mix_shell_input, :yes?, true})
 
       Gen.Auth.run(
         ~w(Accounts User users --no-compile),
@@ -245,7 +229,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ "Mailer.deliver(email)"
         assert file =~ ~s|from({"MyApp", "contact@example.com"})|
         assert file =~ ~s|deliver(user.email, "Confirmation instructions",|
-        assert file =~ ~s|deliver(user.email, "Reset password instructions",|
+        assert file =~ ~s|deliver(user.email, "Log in instructions",|
         assert file =~ ~s|deliver(user.email, "Update email instructions",|
       end)
 
@@ -253,16 +237,10 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       assert_file("test/my_app_web/live/user_live/registration_test.exs")
       assert_file("lib/my_app_web/live/user_live/login.ex")
       assert_file("test/my_app_web/live/user_live/login_test.exs")
-      assert_file("lib/my_app_web/live/user_live/reset_password.ex")
-      assert_file("test/my_app_web/live/user_live/reset_password_test.exs")
-      assert_file("lib/my_app_web/live/user_live/forgot_password.ex")
-      assert_file("test/my_app_web/live/user_live/forgot_password_test.exs")
       assert_file("lib/my_app_web/live/user_live/settings.ex")
       assert_file("test/my_app_web/live/user_live/settings_test.exs")
       assert_file("lib/my_app_web/live/user_live/confirmation.ex")
       assert_file("test/my_app_web/live/user_live/confirmation_test.exs")
-      assert_file("lib/my_app_web/live/user_live/confirmation_instructions.ex")
-      assert_file("test/my_app_web/live/user_live/confirmation_instructions_test.exs")
 
       assert_file("lib/my_app_web/user_auth.ex")
       assert_file("test/my_app_web/user_auth_test.exs")
@@ -285,20 +263,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ """
                  ## Authentication routes
 
-                 scope "/", MyAppWeb do
-                   pipe_through [:browser, :redirect_if_user_is_authenticated]
-
-                   live_session :redirect_if_user_is_authenticated,
-                     on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
-                     live "/users/register", UserLive.Registration, :new
-                     live "/users/log-in", UserLive.Login, :new
-                     live "/users/reset-password", UserLive.ForgotPassword, :new
-                     live "/users/reset-password/:token", UserLive.ResetPassword, :edit
-                   end
-
-                   post "/users/log-in", UserSessionController, :create
-                 end
-
                  scope "/", MyAppWeb do
                    pipe_through [:browser, :require_authenticated_user]
 
@@ -307,18 +271,22 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                      live "/users/settings", UserLive.Settings, :edit
                      live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
                    end
+
+                   post "/users/update-password", UserSessionController, :update_password
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log-out", UserSessionController, :delete
-
                    live_session :current_user,
                      on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
-                     live "/users/confirm/:token", UserLive.Confirmation, :edit
-                     live "/users/confirm", UserLive.ConfirmationInstructions, :new
+                     live "/users/register", UserLive.Registration, :new
+                     live "/users/log-in", UserLive.Login, :new
+                     live "/users/log-in/:token", UserLive.Confirmation, :new
                    end
+
+                   post "/users/log-in", UserSessionController, :create
+                   delete "/users/log-out", UserSessionController, :delete
                  end
                """
       end)
@@ -338,8 +306,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       end)
 
       assert_file("test/support/conn_case.ex", fn file ->
-        assert file =~ "def register_and_log_in_user(%{conn: conn})"
-        assert file =~ "def log_in_user(conn, user)"
+        assert file =~ "def register_and_log_in_user(%{conn: conn} = context)"
+        assert file =~ "def log_in_user(conn, user, opts \\\\ [])"
       end)
 
       assert_received {:mix_shell, :info,
@@ -382,20 +350,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ """
                  ## Authentication routes
 
-                 scope "/", MyAppWeb do
-                   pipe_through [:browser, :redirect_if_user_is_authenticated]
-
-                   live_session :redirect_if_user_is_authenticated,
-                     on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
-                     live "/users/register", UserLive.Registration, :new
-                     live "/users/log-in", UserLive.Login, :new
-                     live "/users/reset-password", UserLive.ForgotPassword, :new
-                     live "/users/reset-password/:token", UserLive.ResetPassword, :edit
-                   end
-
-                   post "/users/log-in", UserSessionController, :create
-                 end
-
                  scope "/", MyAppWeb do
                    pipe_through [:browser, :require_authenticated_user]
 
@@ -404,18 +358,22 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                      live "/users/settings", UserLive.Settings, :edit
                      live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
                    end
+
+                   post "/users/update-password", UserSessionController, :update_password
                  end
 
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
-                   delete "/users/log-out", UserSessionController, :delete
-
                    live_session :current_user,
                      on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
-                     live "/users/confirm/:token", UserLive.Confirmation, :edit
-                     live "/users/confirm", UserLive.ConfirmationInstructions, :new
+                     live "/users/register", UserLive.Registration, :new
+                     live "/users/log-in", UserLive.Login, :new
+                     live "/users/log-in/:token", UserLive.Confirmation, :new
                    end
+
+                   post "/users/log-in", UserSessionController, :create
+                   delete "/users/log-out", UserSessionController, :delete
                  end
                """
       end)
@@ -455,12 +413,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log-in", UserSessionController, :new
-                   post "/users/log-in", UserSessionController, :create
-                   get "/users/reset-password", UserResetPasswordController, :new
-                   post "/users/reset-password", UserResetPasswordController, :create
-                   get "/users/reset-password/:token", UserResetPasswordController, :edit
-                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/", MyAppWeb do
@@ -474,11 +426,10 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  scope "/", MyAppWeb do
                    pipe_through [:browser]
 
+                   get "/users/log-in", UserSessionController, :new
+                   get "/users/log-in/:token", UserSessionController, :confirm
+                   post "/users/log-in", UserSessionController, :create
                    delete "/users/log-out", UserSessionController, :delete
-                   get "/users/confirm", UserConfirmationController, :new
-                   post "/users/confirm", UserConfirmationController, :create
-                   get "/users/confirm/:token", UserConfirmationController, :edit
-                   post "/users/confirm/:token", UserConfirmationController, :update
                  end
                """
       end)
@@ -487,7 +438,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
   test "generates with --web option", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, false}
+      send(self(), {:mix_shell_input, :yes?, false})
 
       Gen.Auth.run(
         ~w(Accounts User users --web warehouse --no-compile),
@@ -514,35 +465,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ "defmodule MyAppWeb.Warehouse.UserAuthTest do"
       end)
 
-      assert_file("lib/my_app_web/controllers/warehouse/user_confirmation_html.ex", fn file ->
-        assert file =~ "defmodule MyAppWeb.Warehouse.UserConfirmationHTML do"
-      end)
-
-      assert_file("lib/my_app_web/controllers/warehouse/user_confirmation_html/new.html.heex", fn file ->
-        assert file =~
-                 ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/confirm"}>)
-
-        assert file =~
-                 ~r|<\.link.*href={~p"/warehouse/users/register"}.*>|s
-
-        assert file =~
-                 ~r|<\.link.*href={~p"/warehouse/users/log-in"}.*>|s
-      end)
-
-      assert_file(
-        "lib/my_app_web/controllers/warehouse/user_confirmation_controller.ex",
-        fn file ->
-          assert file =~ "defmodule MyAppWeb.Warehouse.UserConfirmationController do"
-        end
-      )
-
-      assert_file(
-        "test/my_app_web/controllers/warehouse/user_confirmation_controller_test.exs",
-        fn file ->
-          assert file =~ "defmodule MyAppWeb.Warehouse.UserConfirmationControllerTest do"
-        end
-      )
-
       assert_file("lib/my_app_web/components/layouts/root.html.heex", fn file ->
         assert file =~
                  ~r|<\.link.*href={~p"/warehouse/users/settings"}.*>|s
@@ -575,55 +497,31 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         end
       )
 
-      assert_file(
-        "lib/my_app_web/controllers/warehouse/user_reset_password_controller.ex",
-        fn file ->
-          assert file =~ "defmodule MyAppWeb.Warehouse.UserResetPasswordController do"
-        end
-      )
+      assert_file("lib/my_app_web/controllers/warehouse/user_session_controller.ex", fn file ->
+        assert file =~ "defmodule MyAppWeb.Warehouse.UserSessionController do"
+      end)
 
       assert_file(
-        "lib/my_app_web/controllers/warehouse/user_reset_password_html/edit.html.heex",
+        "lib/my_app_web/controllers/warehouse/user_session_html/new.html.heex",
         fn file ->
           assert file =~
-                   ~S|<.simple_form :let={f} for={@changeset} action={~p"/warehouse/users/reset-password/#{@token}"}>|
-        end
-      )
+                   ~S|<.simple_form :let={f} for={@form} as={:user} id="login_form_magic" action={~p"/warehouse/users/log-in"}>|
+
+          assert file =~ """
+                   <.simple_form
+                     :let={f}
+                     for={@form}
+                     as={:user}
+                     id="login_form_password"
+                     action={~p"/warehouse/users/log-in"}
+                   >
+                 """
 
-      assert_file(
-        "lib/my_app_web/controllers/warehouse/user_reset_password_html/new.html.heex",
-        fn file ->
           assert file =~
-                   ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/reset-password"}>)
+                   ~S|navigate={~p"/warehouse/users/register"}|
         end
       )
 
-      assert_file("lib/my_app_web/controllers/warehouse/user_reset_password_html.ex", fn file ->
-        assert file =~ "defmodule MyAppWeb.Warehouse.UserResetPasswordHTML do"
-      end)
-
-      assert_file(
-        "test/my_app_web/controllers/warehouse/user_reset_password_controller_test.exs",
-        fn file ->
-          assert file =~ "defmodule MyAppWeb.Warehouse.UserResetPasswordControllerTest do"
-        end
-      )
-
-      assert_file("lib/my_app_web/controllers/warehouse/user_session_controller.ex", fn file ->
-        assert file =~ "defmodule MyAppWeb.Warehouse.UserSessionController do"
-      end)
-
-      assert_file("lib/my_app_web/controllers/warehouse/user_session_html/new.html.heex", fn file ->
-        assert file =~
-                 ~S|<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/log-in"}>|
-
-        assert file =~
-                 ~S|<.link navigate={~p"/warehouse/users/register"}|
-
-        assert file =~
-                 ~S|<.link href={~p"/warehouse/users/reset-password"}|
-      end)
-
       assert_file(
         "test/my_app_web/controllers/warehouse/user_session_controller_test.exs",
         fn file ->
@@ -639,13 +537,16 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert file =~ "defmodule MyAppWeb.Warehouse.UserSettingsController do"
       end)
 
-      assert_file("lib/my_app_web/controllers/warehouse/user_settings_html/edit.html.heex", fn file ->
-        assert file =~
-                 ~S|<.simple_form :let={f} for={@email_changeset} action={~p"/warehouse/users/settings"} id="update_email">|
+      assert_file(
+        "lib/my_app_web/controllers/warehouse/user_settings_html/edit.html.heex",
+        fn file ->
+          assert file =~
+                   ~S|<.simple_form :let={f} for={@email_changeset} action={~p"/warehouse/users/settings"} id="update_email">|
 
-        assert file =~
-                 ~s|<.simple_form\n      :let={f}\n      for={@password_changeset}\n      action={~p"/warehouse/users/settings"}\n      id="update_password"\n    >|
-      end)
+          assert file =~
+                   ~s|<.simple_form\n      :let={f}\n      for={@password_changeset}\n      action={~p"/warehouse/users/settings"}\n      id="update_password"\n    >|
+        end
+      )
 
       assert_file("lib/my_app_web/controllers/warehouse/user_settings_html.ex", fn file ->
         assert file =~ "defmodule MyAppWeb.Warehouse.UserSettingsHTML do"
@@ -677,12 +578,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
                    get "/users/register", UserRegistrationController, :new
                    post "/users/register", UserRegistrationController, :create
-                   get "/users/log-in", UserSessionController, :new
-                   post "/users/log-in", UserSessionController, :create
-                   get "/users/reset-password", UserResetPasswordController, :new
-                   post "/users/reset-password", UserResetPasswordController, :create
-                   get "/users/reset-password/:token", UserResetPasswordController, :edit
-                   put "/users/reset-password/:token", UserResetPasswordController, :update
                  end
 
                  scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
@@ -696,18 +591,17 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
                  scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
                    pipe_through [:browser]
 
+                   get "/users/log-in", UserSessionController, :new
+                   get "/users/log-in/:token", UserSessionController, :confirm
+                   post "/users/log-in", UserSessionController, :create
                    delete "/users/log-out", UserSessionController, :delete
-                   get "/users/confirm", UserConfirmationController, :new
-                   post "/users/confirm", UserConfirmationController, :create
-                   get "/users/confirm/:token", UserConfirmationController, :edit
-                   post "/users/confirm/:token", UserConfirmationController, :update
                  end
                """
       end)
 
       assert_file("test/support/conn_case.ex", fn file ->
-        assert file =~ "def register_and_log_in_user(%{conn: conn})"
-        assert file =~ "def log_in_user(conn, user)"
+        assert file =~ "def register_and_log_in_user(%{conn: conn} = context)"
+        assert file =~ "def log_in_user(conn, user, opts \\\\ [])"
       end)
     end)
   end
@@ -715,7 +609,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
   describe "--database option" do
     test "when the database is postgres", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -735,13 +629,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/use MyAppWeb\.ConnCase, async: true$/m
         end)
 
-        assert_file(
-          "test/my_app_web/controllers/user_confirmation_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase, async: true$/m
-          end
-        )
-
         assert_file(
           "test/my_app_web/controllers/user_registration_controller_test.exs",
           fn file ->
@@ -749,13 +636,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           end
         )
 
-        assert_file(
-          "test/my_app_web/controllers/user_reset_password_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase, async: true$/m
-          end
-        )
-
         assert_file("test/my_app_web/controllers/user_session_controller_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase, async: true$/m
         end)
@@ -768,7 +648,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
     test "when the database is mysql", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -788,13 +668,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
 
-        assert_file(
-          "test/my_app_web/controllers/user_confirmation_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file(
           "test/my_app_web/controllers/user_registration_controller_test.exs",
           fn file ->
@@ -802,13 +675,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           end
         )
 
-        assert_file(
-          "test/my_app_web/controllers/user_reset_password_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file("test/my_app_web/controllers/user_session_controller_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
@@ -821,7 +687,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
     test "when the database is sqlite3", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -841,13 +707,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
 
-        assert_file(
-          "test/my_app_web/controllers/user_confirmation_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file(
           "test/my_app_web/controllers/user_registration_controller_test.exs",
           fn file ->
@@ -855,13 +714,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           end
         )
 
-        assert_file(
-          "test/my_app_web/controllers/user_reset_password_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file("test/my_app_web/controllers/user_session_controller_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
@@ -874,7 +726,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
     test "when the database is mssql", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -894,13 +746,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
 
-        assert_file(
-          "test/my_app_web/controllers/user_confirmation_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file(
           "test/my_app_web/controllers/user_registration_controller_test.exs",
           fn file ->
@@ -908,13 +753,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           end
         )
 
-        assert_file(
-          "test/my_app_web/controllers/user_reset_password_controller_test.exs",
-          fn file ->
-            assert file =~ ~r/use MyAppWeb\.ConnCase$/m
-          end
-        )
-
         assert_file("test/my_app_web/controllers/user_session_controller_test.exs", fn file ->
           assert file =~ ~r/use MyAppWeb\.ConnCase$/m
         end)
@@ -928,37 +766,42 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
   test "allows utc_datetime", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, false}
-      with_generator_env(:my_app, [timestamp_type: :utc_datetime], fn ->
+      send(self(), {:mix_shell_input, :yes?, false})
 
+      with_generator_env(:my_app, [timestamp_type: :utc_datetime], fn ->
         Gen.Auth.run(
-        ~w(Accounts User users --no-compile),
-        ecto_adapter: Ecto.Adapters.Postgres
+          ~w(Accounts User users --no-compile),
+          ecto_adapter: Ecto.Adapters.Postgres
         )
 
         assert [migration] = Path.wildcard("priv/repo/migrations/*_create_users_auth_tables.exs")
 
-        assert_file migration, fn file ->
+        assert_file(migration, fn file ->
           assert file =~ "timestamps(type: :utc_datetime)"
           assert file =~ "timestamps(type: :utc_datetime, updated_at: false)"
-        end
+        end)
 
-        assert_file "lib/my_app/accounts/user.ex", fn file ->
+        assert_file("lib/my_app/accounts/user.ex", fn file ->
           assert file =~ "field :confirmed_at, :utc_datetime"
           assert file =~ "timestamps(type: :utc_datetime)"
           assert file =~ "now = DateTime.utc_now() |> DateTime.truncate(:second)"
-        end
+        end)
 
-        assert_file "lib/my_app/accounts/user_token.ex", fn file ->
+        assert_file("lib/my_app/accounts/user_token.ex", fn file ->
           assert file =~ "timestamps(type: :utc_datetime, updated_at: false)"
-        end
+        end)
+
+        assert_file("lib/my_app/accounts.ex", fn file ->
+          assert file =~
+                   "sudo_mode?(%User{authenticated_at: ts}, minutes) when is_struct(ts, DateTime)"
+        end)
       end)
     end)
   end
 
   test "supports --binary-id option", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, false}
+      send(self(), {:mix_shell_input, :yes?, false})
 
       Gen.Auth.run(
         ~w(Accounts User users --binary-id --no-compile),
@@ -990,7 +833,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
   describe "--hashing-lib option" do
     test "when bcrypt", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --hashing-lib bcrypt --no-compile),
@@ -1015,7 +858,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
     test "when pbkdf2", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --hashing-lib pbkdf2 --no-compile),
@@ -1040,7 +883,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
     test "when argon2", config do
       in_tmp_phx_project(config.test, fn ->
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --hashing-lib argon2 --no-compile),
@@ -1068,7 +911,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
   test "with --table option", config do
     in_tmp_phx_project(config.test, fn ->
-      send self(), {:mix_shell_input, :yes?, false}
+      send(self(), {:mix_shell_input, :yes?, false})
 
       Gen.Auth.run(
         ~w(Accounts User users --table my_users --no-compile),
@@ -1099,7 +942,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       in_tmp_phx_umbrella_project(config.test, fn ->
         in_project(:my_app, "apps/my_app", fn _module ->
           with_generator_env(:my_app_web, [context_app: nil], fn ->
-            send self(), {:mix_shell_input, :yes?, false}
+            send(self(), {:mix_shell_input, :yes?, false})
 
             Gen.Auth.run(
               ~w(Accounts User users --no-compile),
@@ -1118,13 +961,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_file("apps/my_app/test/support/fixtures/accounts_fixtures.ex")
         assert_file("apps/my_app/lib/my_app_web/user_auth.ex")
         assert_file("apps/my_app/test/my_app_web/user_auth_test.exs")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_confirmation_html.ex")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_confirmation_html/new.html.heex")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_confirmation_controller.ex")
-
-        assert_file(
-          "apps/my_app/test/my_app_web/controllers/user_confirmation_controller_test.exs"
-        )
 
         assert_file("apps/my_app/lib/my_app_web/controllers/user_registration_controller.ex")
         assert_file("apps/my_app/lib/my_app_web/controllers/user_registration_html.ex")
@@ -1133,15 +969,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           "apps/my_app/test/my_app_web/controllers/user_registration_controller_test.exs"
         )
 
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_reset_password_controller.ex")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_reset_password_html/edit.html.heex")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_reset_password_html/new.html.heex")
-        assert_file("apps/my_app/lib/my_app_web/controllers/user_reset_password_html.ex")
-
-        assert_file(
-          "apps/my_app/test/my_app_web/controllers/user_reset_password_controller_test.exs"
-        )
-
         assert_file("apps/my_app/lib/my_app_web/controllers/user_session_controller.ex")
         assert_file("apps/my_app/lib/my_app_web/controllers/user_session_html/new.html.heex")
         assert_file("apps/my_app/test/my_app_web/controllers/user_session_controller_test.exs")
@@ -1157,7 +984,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       in_tmp_phx_umbrella_project(config.test, fn ->
         in_project(:my_app_web, "apps/my_app_web", fn _module ->
           with_generator_env(:my_app_web, [context_app: :my_app], fn ->
-            send self(), {:mix_shell_input, :yes?, false}
+            send(self(), {:mix_shell_input, :yes?, false})
 
             Gen.Auth.run(
               ~w(Accounts User users --no-compile),
@@ -1176,13 +1003,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_file("apps/my_app/test/support/fixtures/accounts_fixtures.ex")
         assert_file("apps/my_app_web/lib/my_app_web/user_auth.ex")
         assert_file("apps/my_app_web/test/my_app_web/user_auth_test.exs")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_confirmation_html.ex")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_confirmation_html/new.html.heex")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_confirmation_controller.ex")
-
-        assert_file(
-          "apps/my_app_web/test/my_app_web/controllers/user_confirmation_controller_test.exs"
-        )
 
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_registration_controller.ex")
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_registration_html.ex")
@@ -1191,18 +1011,6 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           "apps/my_app_web/test/my_app_web/controllers/user_registration_controller_test.exs"
         )
 
-        assert_file(
-          "apps/my_app_web/lib/my_app_web/controllers/user_reset_password_controller.ex"
-        )
-
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_reset_password_html/edit.html.heex")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_reset_password_html/new.html.heex")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_reset_password_html.ex")
-
-        assert_file(
-          "apps/my_app_web/test/my_app_web/controllers/user_reset_password_controller_test.exs"
-        )
-
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_session_controller.ex")
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_session_html/new.html.heex")
 
@@ -1212,7 +1020,11 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_session_html.ex")
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_settings_controller.ex")
-        assert_file("apps/my_app_web/lib/my_app_web/controllers/user_settings_html/edit.html.heex")
+
+        assert_file(
+          "apps/my_app_web/lib/my_app_web/controllers/user_settings_html/edit.html.heex"
+        )
+
         assert_file("apps/my_app_web/lib/my_app_web/controllers/user_settings_html.ex")
 
         assert_file(
@@ -1242,7 +1054,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       in_tmp_phx_project(config.test, fn ->
         File.write!("mix.exs", "")
 
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -1274,7 +1086,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           String.replace(file, "use MyAppWeb, :router", "")
         end)
 
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -1309,7 +1121,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           String.replace(file, "plug :put_secure_browser_headers\n", "")
         end)
 
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -1340,7 +1152,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         File.rm!("lib/my_app_web/components/layouts/root.html.heex")
         File.rm!("lib/my_app_web/components/layouts/app.html.heex")
 
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -1353,60 +1165,60 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
         assert error == """
 
-        Unable to find an application layout file to inject user menu items.
-
-        Missing files:
-
-          * lib/my_app_web/components/layouts/root.html.heex
-          * lib/my_app_web/components/layouts/app.html.heex
-
-        Please ensure this phoenix app was not generated with
-        --no-html. If you have changed the name of your application
-        layout file, please add the following code to it where you'd
-        like the user menu items to be rendered.
-
-            <ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
-              <%= if @current_user do %>
-                <li class="text-[0.8125rem] leading-6 text-zinc-900">
-                  {@current_user.email}
-                </li>
-                <li>
-                  <.link
-                    href={~p"/users/settings"}
-                    class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-                  >
-                    Settings
-                  </.link>
-                </li>
-                <li>
-                  <.link
-                    href={~p"/users/log-out"}
-                    method="delete"
-                    class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-                  >
-                    Log out
-                  </.link>
-                </li>
-              <% else %>
-                <li>
-                  <.link
-                    href={~p"/users/register"}
-                    class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-                  >
-                    Register
-                  </.link>
-                </li>
-                <li>
-                  <.link
-                    href={~p"/users/log-in"}
-                    class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
-                  >
-                    Log in
-                  </.link>
-                </li>
-              <% end %>
-            </ul>
-        """
+               Unable to find an application layout file to inject user menu items.
+
+               Missing files:
+
+                 * lib/my_app_web/components/layouts/root.html.heex
+                 * lib/my_app_web/components/layouts/app.html.heex
+
+               Please ensure this phoenix app was not generated with
+               --no-html. If you have changed the name of your application
+               layout file, please add the following code to it where you'd
+               like the user menu items to be rendered.
+
+                   <ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
+                     <%= if @current_user do %>
+                       <li class="text-[0.8125rem] leading-6 text-zinc-900">
+                         {@current_user.email}
+                       </li>
+                       <li>
+                         <.link
+                           href={~p"/users/settings"}
+                           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
+                         >
+                           Settings
+                         </.link>
+                       </li>
+                       <li>
+                         <.link
+                           href={~p"/users/log-out"}
+                           method="delete"
+                           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
+                         >
+                           Log out
+                         </.link>
+                       </li>
+                     <% else %>
+                       <li>
+                         <.link
+                           href={~p"/users/register"}
+                           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
+                         >
+                           Register
+                         </.link>
+                       </li>
+                       <li>
+                         <.link
+                           href={~p"/users/log-in"}
+                           class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
+                         >
+                           Log in
+                         </.link>
+                       </li>
+                     <% end %>
+                   </ul>
+               """
       end)
     end
 
@@ -1416,7 +1228,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
           ""
         end)
 
-        send self(), {:mix_shell_input, :yes?, false}
+        send(self(), {:mix_shell_input, :yes?, false})
 
         Gen.Auth.run(
           ~w(Accounts User users --no-compile),
@@ -1483,7 +1295,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
       File.mkdir_p!("priv/templates/phx.gen.auth")
       File.write!("priv/templates/phx.gen.auth/auth.ex", "#it works!")
 
-      send self(), {:mix_shell_input, :yes?, false}
+      send(self(), {:mix_shell_input, :yes?, false})
 
       Gen.Auth.run(
         ~w(Accounts Admin admins --no-compile),
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index 450e0599b..53b51e58d 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -396,6 +396,26 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
     end)
   end
 
+  test "with custom primary key", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Html.run(~w(Blog Post posts title:string --primary-key post_id))
+
+      assert_file("lib/phoenix_web/controllers/post_controller.ex", fn file ->
+        assert file =~ ~s[%{"post_id" => post_id}]
+        assert file =~ ~s[%{"post_id" => post_id, "post" => post_params}]
+        assert file =~ ~s[Blog.get_post!(post_id)]
+      end)
+
+      assert_file("lib/phoenix_web/controllers/post_html/show.html.heex", fn file ->
+        assert file =~ ~S(Post {@post.post_id})
+      end)
+
+      assert_file("lib/phoenix_web/controllers/post_html/edit.html.heex", fn file ->
+        assert file =~ ~S(Edit Post {@post.post_id})
+      end)
+    end)
+  end
+
   describe "inside umbrella" do
     test "without context_app generators config uses web dir", config do
       in_tmp_umbrella_project(config.test, fn ->
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index 32fb2112f..696b000a0 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -354,4 +354,24 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
       :code.delete(module)
     end)
   end
+
+  test "with custom primary key", config do
+    in_tmp_project(config.test, fn ->
+      Gen.Json.run(~w(Blog Post posts title:string --primary-key post_id))
+
+      assert_file "lib/phoenix_web/controllers/post_controller.ex", fn file ->
+        refute file =~ ~s[%{"id" =>]
+        assert file =~ ~s[%{"post_id" =>]
+        assert file =~ "Blog.get_post!(post_id)"
+      end
+
+      assert_file "lib/phoenix_web/controllers/post_json.ex", fn file ->
+        assert file =~ "post_id: post.post_id,"
+      end
+
+      assert_file "test/phoenix_web/controllers/post_controller_test.exs", fn file ->
+        assert file =~ ~s["post_id" => ^post_id,]
+      end
+    end)
+  end
 end
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index 303fc4db4..2bc0661dc 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -74,6 +74,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
 
       assert_file "lib/phoenix_web/live/post_live/index.ex", fn file ->
         assert file =~ "defmodule PhoenixWeb.PostLive.Index"
+        refute file =~ "dom_id:"
       end
 
       assert_file "lib/phoenix_web/live/post_live/show.ex", fn file ->
@@ -434,6 +435,29 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
     end
   end
 
+  test "with custom primary key", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title:string --primary-key post_id))
+
+      assert_file "lib/phoenix_web/live/post_live/index.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Index"
+        assert file =~ ~S[dom_id: &"posts-#{&1.post_id}"]
+        assert file =~ ~s[JS.push("delete", value: %{post_id: post.post_id})]
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/show.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Show"
+        assert file =~ ~s[def mount(%{"post_id" => post_id}, _session, socket)]
+        assert file =~ "Blog.get_post!(post_id)"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Form"
+        assert file =~ ~s[defp apply_action(socket, :edit, %{"post_id" => post_id}) do]
+      end
+    end
+  end
+
   test "raises on schema named form" do
     assert_raise Mix.Error, ~r/cannot use form as the schema name because it conflicts with the LiveView assigns/, fn ->
       Mix.Tasks.Phx.Gen.Live.run(~w(Blog Form forms title:string))