Subscribe to access all episodes. View plans →

#118: Password Reset

Published July 13, 2020

Elixir 1.10

Phoenix 1.5

SecureRandom 0.5


Here we have an Elixir Phoenix application where we can sign in or sign up. But, if we sign up and then forget our password, we don’t have a way to reset it.

In this episode, let’s implement password reset functionality where a user can submit a new password. The basic flow will be, a user submits a form with their email, we’ll check if we have that email registered, and if it is, we’ll send an email with a link they can use to reset their password.

Our app already has Bamboo setup to send emails, which will save us some time. If you’re not familiar with Bamboo check out episodes 14 and 15.

We currently have one schema already setup for our users. Let’s take a quick look it and currently it has a field for the encrypted_password and the email. Our password reset URL will have a password reset token that will expire after 2 hours. We’ll be adding two fields to our user schema here, one for the reset token and one for the timestamp to check if it’s still valid.

Alright, let’s start by creating a controller to handle our password resets. We’ll create a new controller named password_reset_controller.ex. Then we’ll define our module and we’ll create a “new” action to render our password reset page. Then define the “new” function, accepting the conn and then we’ll ignore the params.

lib/teacher_web/controllers/password_reset_controller.ex

defmodule TeacherWeb.PasswordResetController do
  use TeacherWeb, :controller

  def new(conn, _params) do
    render(conn, "new.html")
  end
  
end

Then let’s create the corresponding password_reset_view.ex and inside it we’ll define the module.

lib/teacher_web/views/password_reset_view.ex

defmodule TeacherWeb.PasswordResetView do
  use TeacherWeb, :view

end

Then let’s create a template to render our password reset form. We’ll create a “password_reset” directory in templates.

Then we’ll define the new.html.eex template. I’ll paste in the template here. This form will post to the password reset controller create action and it only has one input for the email address.

Template path: lib/teacher_web/templates/password_reset/new.html.eex

<%= form_for @conn, Routes.password_reset_path(@conn, :create), 
  [class: "elixircasts-form"], fn f -> %>
  <%= text_input f, :email,
    placeholder: "email",
    required: true %>
  <%= error_tag f, :email %>

  <div>
    <%= submit "Send password reset email" %>
  </div>
<% end %>

Now let’s create the routes that will handle password resets. We’ll open our the router.ex and add the “/password-reset” resource except for the “index” and “show” actions, since we wont need those for our password reset functionality.

lib/teacher_web/router.ex

...
resources "/password-reset", PasswordResetController, except: [:index, :show]
...

And let’s add a link to our password reset on our sign in page. We’ll open the session new.html.eex template. And add a “Forgot password?” link to the bottom of the page.

...
<%= link "Forgot password?", to: Routes.password_reset_path(@conn, :new) %>

Now let’s start up our server.

$ mix phx.server
...

And go to the sign-in page in the browser and great we see our link is being displayed. And our password reset page loads. Now If we submit the form with an email we get an error - we need to implement the PasswordReset.create function that’s handling our form’s post. Let’s do that now.

Going back to our password_reset_controller.ex we’ll define the create function, taking the connection, and then let’s pattern match on the params the get the “email” that was submitted. Now our password reset will need to do a few things. We’ll create the password reset token that will only be valid for 2 hours and then we’ll send the password reset email to the user if they exist in the database.

To lookup a user by their email, we can use the Accounts.get_user_from_email function. Let’s go ahead and use that to get our user from the email address submitted in the form. If no user is found we’ll add a flash message and redirect to the password reset path. Then if we return a user we’ll need to create a token and then deliver an email to the user with a link using that token that the user can use to reset their email.

Once we’ve sent the email, we’ll add a flash message letting them know an emails has been sent and redirect the user back to the password reset page. Then let’s alias the Accounts module so we can call it without the prefix.

lib/teacher_web/controllers/password_reset_controller.ex

...
alias Teacher.Accounts

...

def create(conn, %{"email" => email}) do
  case Accounts.get_user_from_email(email) do
    nil ->
      conn
      |> put_flash(:info, "No email exists")
      |> redirect(to: Routes.password_reset_path(conn, :new))

    user ->
      #create token
      #send email

      conn
      |> put_flash(:info, " Email sent with password reset instructions")
      |> redirect(to: Routes.password_reset_path(conn, :new))

  end
end
...

We’ll want to store the password reset token on the user along with a timestamp of when it was created. Let’s go to the command line and create a migration to add those to our user.

$ mix ecto.gen.migration add_pass_reset_token_to_users
creating priv/repo/migrations/{timestamp}_add_token_to_users.exs

Then let’s open the migration and add the password_reset_token as a string and the password_reset_sent_at as a naive_datetime.

priv/repo/migrations/{timestamp}_add_pass_reset_token_to_users.ex

defmodule Teacher.Repo.Migrations.AddPassResetTokenToUsers do
use Ecto.Migration

  def change do
    alter table("users") do
      add :password_reset_token, :string
      add :password_reset_sent_at, :naive_datetime
    end
  end
end

Then let’s run our migration.

$ mix ecto.migrate
...

And add the two fields to our User schema, and to the cast function in our changeset.

lib/teacher/accounts/user.ex

...

schema "users" do
  field :encrypted_password, :string
  field :email, :string
  field :password_reset_token, :string
  field :password_reset_sent_at, :naive_datetime

  timestamps()
end

def changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :encrypted_password, :password_reset_token, :password_reset_sent_at])
  ...
end

...

Great, now that we have our field created, let’s add a function we can call that will generate and save the token. Let’s open our accounts.ex module. And we’ll create a function named set_token_on_user that will take a user.

Now we’ll need to create the actual token and to do that, let’s use the secure_random package. Let’s open our Mixfile and add secure_random to our list of dependencies.

mix.exs

...
{:secure_random, "~> 0.5"},
...

Then let’s go to the command line and download our dependency.

$ mix deps.get
...
New:
  secure_random 0.5.1

Now let’s go back to the Accounts module. We’ll use SecureRandom.urlsafe_base64 to generate a random URL safe base 64 string that we’ll use for our password_reset_token. Then for the “password_reset_sent_at” we’ll use NaiveDateTime.utc_now.

Once we have those we can update our user with the new attributes. While we’re here, let’s create another function that we know we’ll need, and that’s to lookup a user by their token. We’ll define a new function named get_user_from_token that will take a token. Then let’s call Repo.get_by to get the user with the matching token.

lib/teacher/accounts.ex

...

def get_user_from_token(token) do
  Repo.get_by(User, password_reset_token: token)
end

def set_token_on_user(user) do
  attrs = %{
    "password_reset_token" => SecureRandom.urlsafe_base64(),
    "password_reset_sent_at" => NaiveDateTime.utc_now()
  }

  user
  |> User.changeset(attrs)
  |> Repo.update!()
end

...

Now let’s go back to the password_reset_controller.ex and update the create function to include our new Accounts.set_token_on_user function.

lib/teacher_web/controllers/password_reset_controller.ex

...

user ->
  user
  |> Accounts.set_token_on_user()
  #send email
  
...

We’ll still need to send a password reset email to the user , but before we do that let’s create the page that will allow a user to change their password.

Let’s define the edit function to handle our edit action, taking our connection, then let’s pattern match to get our token from the params and let’s take the token and use it to get our user.

If we return a user, we’ll create a user changeset we can use in the update password form. Then we’ll render the “edit.html” template with the changeset and the user in the assigns. If no user is found, we’ll render an “invalid_token.html” template.

lib/teacher_web/controllers/password_reset_controller.ex

...

def edit(conn, %{"id" => token}) do
  if user = Accounts.get_user_from_token(token) do
    changeset = Accounts.change_user(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  else
    render(conn, "invalid_token.html")
  end
end

...

Now let’s create that corresponding “edit.html” template. We’ll create the template. Then I’ll paste in password update form, but let’s walkthrough it.

We’re sending our form data to the password reset’s update action and in our form we have one input group for the encrypted_password and then a second group will be for the encrypted_password_confirmation.

This will require the user to enter their new password twice in order to reduce the chance there’s a typo in the new password. We’ll go over this more in a just a minute, but the reason we’ve chosen to append “confirmation” to the field here is because we’ll be using the Ecto.Changeset.validate_confirmation function to ensure that the value of these two inputs match.

Template path: lib/teacher_web/templates/password_reset/edit.html.eex

<%= form_for @changeset, Routes.password_reset_path(@conn, :update, @user.password_reset_token), [class: "elixircasts-form"], fn f -> %>
  <%= label f, :encrypted_password, "New password" %>
  <%= password_input f, :encrypted_password, required: true %>
  <%= error_tag f, :encrypted_password %>

  <%= label f, :encrypted_password_confirmation, "New password confirmation" %>
  <%= password_input f, :encrypted_password_confirmation, required: true %>
  <br>
  <%= error_tag f, :encrypted_password_confirmation %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

Now let’s also create the invalid_token.html.eex template. Since this will be rendered if the token isn’t associated with a user, we’ll display a message that the password reset token is invalid and then a link to request a new one.

Template path: lib/teacher_web/templates/password_reset/invalid_token.html.eex

<h1>Invalid Password Rest Token</h1>
<%= link "Request new one", to: Routes.password_reset_path(@conn, :new) %>

Alright, let’s start our server

$ mix phx.server
...

And if we go to the “Sign in” page we see the “Forgot password” link. Let’s try submitting the form again with our email and great it looks like it worked.

Let’s check out the “users” table in the database and we see that the password_reset_token and password_reset_sent_at were both saved to our user. Great, now if we copy the password_reset_token we should be able to use it to access the password rest page. And perfect, we our user is found and we see the password reset form is displayed.

Now if we submit our form here we get an error because our PasswordResetController.update hasn’t been defined yet. Let’s do that now. Let’s go back to our password_reset_controller.ex and we’ll define the update function, taking the controller pattern matching on the params to get the token and the pass_attrs. Inside the function we can use the token to lookup the user. And if we successfully update the users password, let’s reset the password token fields…

Now we want our password_reset_token to only be valid for 2 hours after it’s created. Let’s create a function to check that. We’ll open the accounts.ex module and create a new function named valid_token? that will take the time our token was sent at. Then we’ll get the current time to compare against with NaiveDateTime.utc_now and we’ll compare the two with Time.diff and since 2 hours is 7200 seconds, we’ll say that it’s valid if the returned difference is less than 7200 seconds.

lib/teacher/accounts.ex

...

def valid_token?(token_sent_at) do
  current_time = NaiveDateTime.utc_now()
  Time.diff(current_time, token_sent_at) < 7200
end

...

With that we can go back to the password_reset_controller.ex and finish our function. Let’s use the with statement here to check if our token is valid and if it’s valid, we’ll update the user. Then once the user is updated, we’ll add a message letting them know and then redirect to the sign in page.

Now if the token is invalid we’ll add a message saying that the token has expired and then redirect to the new password reset path. Let’s also handle the case where we’re not able to update the user’s password. In that case, we’ll get back an error tuple with a changeset. Then we’ll add an error message and render the error template with the user and the changeset.

lib/teacher_web/controllers/password_reset_controller.ex

...

def update(conn, %{"id" => token, "user" => pass_attrs}) do
  user = Accounts.get_user_from_token(token)

  pass_attrs =
    Map.merge(pass_attrs, %{"password_reset_token" => nil, "password_reset_sent_at" => nil})

  with true <- Accounts.valid_token?(user.password_reset_sent_at),
       {:ok, _updated_user} <- Accounts.update_user(user, pass_attrs) do

       conn
       |> put_flash(:info, "Your password has been reset. Sign in below with your new password.")
       |> redirect(to: Routes.session_path(conn, :new))

  else
    {:error, changeset} ->
      conn
      |> put_flash(:error, "Problem changing your password")
      |> render("edit.html", user: user, changeset: changeset)

    false ->
      conn
      |> put_flash(:error, "Reset token expired - request new one")
      |> redirect(to: Routes.password_reset_path(conn, :new))

  end
end

...

In order to validate the password, we need to make a quick change to our user.ex module, so let’s open that. And we’ll add the validate_confirmation function to our changeset function. The validate_confirmation function will compare the field with the confirmation field, which is the name of the field we give it with “_confirmation” added to it. Because we added the encrypted_password_confirmation field to the update password form, this should work.

lib/teacher/accounts/user.ex

...
def changeset(user, attrs) do
  ...
  |> validate_confirmation(:encrypted_password)
  ...
end
...

Now let’s test that the password reset works. We’ll go back to the browser. And if we enter a password that doesn’t match, we get an error - our passwords don’t match. Great now let’s try one that matches. It looks like it works, but let’s test it out and our new password works!

Alright, let’s finish the last piece of this and that’s to send the password reset email. To send the email, we’ll use Bamboo, which I mentioned at the start of this video our application is already configured to use it.

If you’ve never used Bamboo and you’re curious how it works, check out episodes 14 and 15. Since we have Bamboo setup let’s open the email.ex module and let’s define a function named password_reset that will take a user.

Then inside the function let’s build up our email. We’ll call the Bamboo new_email function and pipe it into the from function, then into the to using the user’s email. And then let’s set the email subject and add the layout, assign the user, and we’ll use a template we’ll call password_reset.html.

lib/teacher/email.ex

defmodule Teacher.Email do
  use Bamboo.Phoenix, view: TeacherWeb.EmailView

  def password_reset(user) do
    new_email()
    |> from("no-reply@elixircasts.io")
    |> to(user.email)
    |> subject("Password reset")
    |> put_html_layout({TeacherWeb.LayoutView, "email.html"})
    |> assign(:user, user)
    |> render("password_reset.html")
  end
end

Now let’s create the password_reset template. We’ll need to create the “email” directory. Then we’ll create a password_reset.html.eex template and a link that will take the user to the password reset edit page.

Template path: lib/teacher_web/templates/email/password_reset.html.eex

<h1>Password Reset</h1>
<%= link "Change password",
  to: Routes.password_reset_url(TeacherWeb.Endpoint, :edit, @user.password_reset_token) %>

And now all we need to do is go back to our password_reset_controller.ex and in our create action after we set the token on the user we’ll pipe the returned user into Email.password_reset and then we can pipe that into Mailer.deliver_later to send our email

Let’s also add aliases for our Email and Mailer modules since we’re calling them without their prefix.

lib/teacher_web/controllers/password_reset_controller.ex

...

alias Teacher.{Email, Mailer, Accounts}

...

user ->
  user
  |> Accounts.set_token_on_user()
  |> Email.password_reset()
  |> Mailer.deliver_later()
  
...

Alright, let’s test everything out. We’ll go to the browser and click that we forgot our password and submit the form with our email.

Since we’re using Bamboo with the LocalAdapter we can go to the /sent_emails path to view our email. And great - our password reset email triggered and we see it displayed.

If we click the link in the email our password reset form is loaded - perfect. And we can go ahead and reset our password right here. And then we should be able to sign in with our new password. We now have a working password reset feature in our application.

© 2024 HEXMONSTER LLC