User authentication from scratch in Elixir and Phoenix
In my previous post about Phoenix, we built a very simple blog app. We only added the ability to post content, but there’s no user authentication system. In this post, we will add user authentication to the app.
There are excellent authentication libraries out there like passport and addict, but here we’ll be writing authentication code from scratch, so that we can get more familiar with the framework.
NOTE: I’m using Elixir 1.2.4 with Phoenix 1.1.4 for this example.
Add a user model
The first thing we will do is add a User model. Our users will sign up with an email and a password. We will not be storing the password directly as plaintext, but in an hashed format. Let’s generate the model:
(Note: If you’re facing problems with the command, you might want to take a look at this issue to solve this. The API to define unique contraint is not available in Phoenix < v1.0.4.)
This adds a User model
and a migration
to create a users
table
in the database.
Run the migration
using the command:
Signup page
Now that we have added the User model,
let’s move on to creating a registration page.
Let’s add the registration routes to web/router.ex
.
Add this line after
the resources "/posts"
line
in the file:
This adds a /registrations
route,
and a POST /registrations
path
to handle the form submission.
Add a link to the register page
in the navigation section of
web/templates/layout/app.html.eex
:
Next we add a RegistrationController
:
When we try to load the /register
page,
Phoenix shows us an error page
complaining about a missing RegistrationView
.
Let’s add an empty view module.
Now, we are ready to add the HTML view to
web/templates/registration/new.html.eex
:
Now if we go to the path /registrations/new
,
we will find a signup form.
If we submit the form,
we will get this error:
Let’s add the
RegistrationController.create/2
function
to handle this request.
The function would look like this:
Here we’re capturing
the email and password into user_params
,
and creating a User
changeset using it.
We will need to create
the Blog.Registration
module
to save the user to database.
We’ll come back to that later.
First, let’s take care
of adding validations
to the User
model.
Validating and persisting User
We need to make some changes
to the User
model
before we can continue.
The first thing to do
is to add a virtual password attribute
to User
in web/models/user.ex
.
This allows us to create a changeset
with the password attribute,
but it will not be persisted.
The schema block should now look like this:
Next we’ll change @required_fields
to make password mandatory
instead of crypted_password.
We also need to add some validations
to the changeset/2
function.
This function makes email and password mandatory,
validates uniqueness of email,
checks if email contains an @
(this is a very crude format validation,
but should suffice for now),
and ensures password is longer than 5 characters.
If we submit the form now, we should see the same template re-rendered with the corresponding errors.
Now let’s return to the case where we have a valid changeset and need to persist the user to the database.
Here, we’re calling Blog.Registration.create/2
,
which is in a module we haven’t written yet.
It hashes the password,
writes the User
record to database,
and returns the persisted object.
Then it redirects the user
to the home page
with the success flash message.
The Registration
module
Now let’s implement the Registration
module.
This will contain a create
function
that takes in a user changeset and a repo,
hash the password using Bcrypt
and then save the changeset to the database.
We haven’t implemented
the function to hash the password.
For this, we will use the
comeonin library
which provides functions
to hash passwords using bcrypt.
Add comeonin to mix.exs
:
Now run mix deps.get
to install the new dependency.
Once this has been installed,
we can implement the hashed_password/2
function like this:
If you submit valid email and password, you will now be redirected to the home page and shown the message, “your account has been created”.
Adding a login page
Now that we have added
the ability to create an account,
let’s add the login/logout feature.
The first thing is to add the routes.
Add these routes immediately after the registration page routes in
web/router.ex
.
Next, let’s add the SessionController and create the login page.
Create a new template
web/templates/session/new.html.eex
and add this:
If you open the page /login
it would complain that
Blog.SessionView
module is missing.
Let’s add an empty module
as we did with RegistrationController
.
This fixes the problem, and you can now see the login page.
Submitting the login form
Now we need to add a function
to handle submission of this form.
This is what the create
function looks like:
This function matches the session params
and passes them to Blog.Session.login/2
.
This function checks
if there is a user in the repo
with the matching password,
and returns a tuple
containing :ok
and the user
if the email and password are correct.
Otherwise it returns the atom :error
.
The Session
module
We haven’t added the Blog.Session
module yet.
Let’s go ahead and write the code.
We can use Comeonin.Bcrypt.checkpw/2
to check if the password matches the user’s password.
If the password is wrong,
or if a user does not exist with that email,
we return the atom :error
.
If authentication succeeds,
we return a tuple
containing :ok
and the user.
Adding some helper functions for the view
You will notice that even when a user is logged in, they can see the register link in the navbar, and access the page. We need to hide this, and show the logout button when a user is logged in.
Let’s add a couple of functions
to Blog.Session
and make them available in the view.
The first function is current_user/1
that returns the currently logged in user.
It uses the :current_user
session variable
we set during login.
We can now use this
to add another helper function, logged_in?
,
to Blog.Session
.
Now we can make these available in the views
by adding this line at the end of
the quote do ..
block in
the Blog.Web.view/0
function
in web/web.ex
:
We can now replace the register page link
with code that shows the login and register links
only when the user isn’t signed in.
Let’s edit web/templates/layout/app.html.eex
:
We have also used the current_user
function
to show the logged in user’s email in the views.
Logout functionality
The last thing we need to do now
is to add the ability for users to logout.
To do this,
we can add the following delete
function
to SessionController
:
If you now click the logout link in the navbar, the user will be logged out correctly.
Automatic login on signup
One final thing we will do
before finishing up with this tutorial
is to log users in immediately after signup.
We will add the following line
in Blog.RegistrationController.create/2
:
This automatically logs in the user after signup. There are many more details that we need to add to turn this into a complete authentication system, but we have already set up the basics.
Next steps
You might have noticed that we can still access login and register pages even when we’re logged in. This is not something we want in a proper authentication system.
We can easily do this using plugs, which I will hopefully cover in a future blog post.
Links
- Phoenix app with authentication - this article was a very useful reference for how to go about implementing the authentication feature.
- Passport and Addict are two Phoenix authentication libraries. Reading their source code was another excellent reference
- Rewriting ElixirStream.com from Rails to Phoenix is another article that describes how to add authentication to a Phoenix app.