Subscribe to access all episodes. View plans →
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.
Max
4 years agoThanks. it is very useful for me.
Alekx
4 years agoYou’re welcome, Max. That’s great to hear!