:unconfirmed_email not being set when user is created (using custom workflow)

Following on from this post - Pow - utilise existing code to prevent change of email address?

I’m not sure if this is because I have a custom controller, or I have user_id_field set to something other than :email - but when the user is registered for the first time, it appears that :unconfirmed_email isn’t set. If I edit the user and change the email address, then it is.

This is the default changeset:

  def changeset( user_or_changeset, attrs ) do
    user_or_changeset
    |> cast(attrs, [:email])
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
  end

And here’s the create part in my RegistrationController:

defmodule ZoinksWeb.RegistrationController do
  use ZoinksWeb, :controller

  def new(conn, _params) do
    changeset = Pow.Plug.change_user(conn)

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.create_user(user_params)
    |> case do
      {:ok, user, conn} ->
        if user.email do
          conn
          |> send_confirmation_email( user )
          |> Zoinks.Pow.recreate_session( user )
          |> redirect( to: Routes.registration_path(conn, :almost) )
        else
          conn
          |> Zoinks.Pow.recreate_session( user )
          |> put_flash(:info, "Welcome!")
          |> redirect(to: Routes.log_path(conn, :index))
        end

      {:error, changeset, conn} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def send_confirmation_email(conn, user) do
    if user && user.email && !user.email_confirmed_at do
      url = Routes.registration_url( conn, :confirmed, user.email_confirmation_token  )
      email = PowEmailConfirmation.Phoenix.Mailer.email_confirmation( conn, user, url )

      Pow.Phoenix.Mailer.deliver(conn, email)

      IO.puts "Pop goes the email!"
    end

    conn
  end
end

Looking at maybe_set_unconfirmed_email - it’s expecting state to be :loaded. However, in my code state is :built

When I edit the user, everything works fine. Did I miss something when creating the user?

That’s the expected behavior. When users sign up the :email is set, when it’s changed :unconfirmed_email is used. This is to ensure that authentication will work (if email is used to auth) when creating a new user, while :unconfirmed_email is used to ensure that :email won’t change until the new email has been confirmed when updating it.

If you are sure that you’ll always have email confirmation enabled, then you can make it pretty simple for yourself:

defmodule ZoinksWeb.RegistrationController do
  use ZoinksWeb, :controller

  def new(conn, _params) do
    changeset = Pow.Plug.change_user(conn)

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.create_user(user_params)
    |> case do
      {:ok, user, conn} ->
        conn
        |> send_confirmation_email( user )
        |> Zoinks.Pow.recreate_session( user )
        |> redirect( to: Routes.registration_path(conn, :almost) )

      {:error, changeset, conn} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def send_confirmation_email(conn, user) do
    url = Routes.registration_url( conn, :confirmed, user.email_confirmation_token  )
    email = PowEmailConfirmation.Phoenix.Mailer.email_confirmation( conn, user, url )

    Pow.Phoenix.Mailer.deliver(conn, email)

    IO.puts "Pop goes the email!"

    conn
  end
end

And if you will have some that can register without having to confirm email, then I woul do something like this:

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.create_user(user_params)
    |> maybe_halt()
  end

  defp maybe_halt({:ok, %{email_confirmed_at: nil, email_confirmation_token: token} = user, conn}) when not is_nil(token) do
    conn
    |> send_confirmation_email( user )
    |> Zoinks.Pow.recreate_session( user )
    |> redirect( to: Routes.registration_path(conn, :almost) )
  end
  defp maybe_halt({:ok, user, conn}) do
    conn
    |> Zoinks.Pow.recreate_session( user )
    |> put_flash(:info, "Welcome!")
    |> redirect(to: Routes.log_path(conn, :index))
  end
  defp maybe_halt({:error, changeset, conn}) do
    render(conn, "new.html", changeset: changeset)
  end

I’ll check where I can update the docs to highlight the above since it’s not obvious how email confirmation actually works, thanks for letting me know :smile:

3 Likes

No problem! I’m a complete noob when it comes to user login/web security! At least I’m being helpful with something.

I’m currently thinking that I should force users to have an email address but allow them to gain access to the site if their email is unverified. Users can change their settings, but are not allowed to do general data entry (this is how GitHub currently works with an unverified primary email address). Then the user feels like they can do something (and may therefore be more motivated to complete the process).

However, I’m sure that this introduces a set of problems that I’m not experienced with (which is probably a bad thing when it comes to security). So I may not end up doing it that way for the first iteration.

Regardless, if I allow the user to login without confirming their email address, then I’m think in addition to this code, I should also do this:

<%= if !@changeset.data.email_confirmed_at do %>
  <div>
    <p>Click the link in the confirmation email to verify your email address <%= @changeset.data.email %></span>.</p>
  </div>
<% end %>

Do you think that makes sense?

I would rewrite it to:

<%= if @changeset.data.email_confirmation_token and !@changeset.data.email_confirmed_at do %>
  <div>
    <p>Click the link in the confirmation email to verify your email address <%= @changeset.data.unconfirmed_email || @changeset.data.email %></span>.</p>
  </div>
<% end %>

This will only show when an email confirmation token has been generated, and also use the unconfirmed email if set. You may only want to show this message to newly registered users. In that case you can ignore the unconfirmed email lookup, but there may be an issue with users who change their email before confirming the one they used to sign up. I would recommend that you add some integration tests here to make sure it works the way you expect it to.

Your use case makes a lot of sense. PowEmailConfirmation is built around being restrictive, but it’s pretty easy to just use it loosely (like you do by depending on the ecto methods, but using custom controllers).

I’m not sure if adding a guide about it to the Pow docs would make sense, but I would appreciate any feedback that can help improve the docs, or code. It can help others in similar situation to get on the right track fast.

I agree, what I’m trying to achieve is more the exception than the rule. So I don’t think it makes sense to make reference to it in the guides.

Part of my problem is learning website security/user authentication such that I can trust what I’m doing is reasonably correct and limits the chances of the site being vulnerable. The Pow guides are very helpful in this circumstance. If I follow the instructions in the guides to create the default setup - then my knowledge of such things can remain shallow and I can get on with the business of creating my website.

Where I have faced difficulty is when I decided to implement custom controllers (in order to create my own workflow). Obviously, that means taking matters into my own hands. As a result, I’ve ended up looking at the Pow source code for inspiration on how to do things the right way. Which didn’t turn out to be as easy as I’d hoped - the code that’s in the callbacks doesn’t really look like what you’d do in your own controller. If I knew what I was doing, then I don’t think that’d be a big issue.

What would be helpful (especially for a beginner) is if there were a step-by-step guide on creating the default Pow setup - but with custom controllers.That would allow me to focus on making sure that the changes I made were good choices, instead of wondering whether I’m doing it right in the first place. Such documentation may highlight that you can depend on the Ecto methods (even if you write your own controllers).

Hopefully you find this information useful…

Just back on the last snippet of code you gave me. If I have an email that is verified (old@email.com), then change it to a new unverified address (new@email.com) - should the confirmation go to old@email.com or new@email.com?

The revised alert displays “Verification email has been sent to new@email.com”. But I’m pretty sure it’s actually going to old@email.com. The text box also displays "old@email.com". It’s all a bit confusing.

This is what my template looks like at the moment:

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
      <ul>
        <%= for {attr, message} <- f.errors do %>
          <li><%= humanize(attr) %> <%= translate_error(message) %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <%= if @changeset.data.unconfirmed_email || !@changeset.data.email_confirmed_at do %>
    <div>
      <div class="alert alert-info alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        Verification email has been sent to <i><%= @changeset.data.unconfirmed_email || @changeset.data.email %></i>. <%= link "Resend Email", to: Routes.user_path(@conn, :send_email), class: "alert-link" %>
      </div>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>

  <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <div class="form-group">
    <%= label f, :bio, class: "control-label" %>
    <%= if @conn.params["bio"] do %>
      <%= textarea f, :bio, class: "form-control", autofocus: "" %>
    <% else %>
      <%= textarea f, :bio, class: "form-control" %>
    <% end %>

    <%= error_tag f, :bio %>
  </div>

  <div class="form-group">
    <%= submit "Update", class: "btn btn-primary" %>
  </div>
<% end %>
1 Like

Thanks for the feedback! The custom controllers guide goes lightly into adding extensions: Custom controllers — Pow v1.0.12

The docs for the extensions should definitely be improved. Only one module has docs in PowEmailConfirmation. I think if e.g. the controller callbacks has a good overview for how the confirmation logic works, it would have been much easier to bridge it.

Oh, you found a bug :fearful: I’ll fix it today.

1 Like

That sounds pretty good.

No need to rush :icon_cool: . Thanks for your help - it’s made doing this so much easier!

Pow v1.0.13 released that both adds a bunch of more documentation, refactor a lot of email confirmation logic, and fixes the above mentioned bug :slight_smile:

2 Likes

E.g. here’s the docs for the controller callbacks explaining how it all works: https://hexdocs.pm/pow/PowEmailConfirmation.Phoenix.ControllerCallbacks.html#content

2 Likes