Hashing passwords with Comeonin
The first thing we need to do is continue where we left off in the previous lesson. We want to turn the user's
password param into an
encrypted_password that we can store safely in our database.
To accomplish this task, we're going to use the Comeonin library, so you'll also learn how to use external Elixir dependencies in your app. Open the file
mix.exs at the root of your project, scroll all the way down to the private
deps function, and add a row for
comeonin right before the list closes:
Now that that's there, we can run this Mix task to install the newly added dependency, and recompile our app (you might also want to restart your Phoenix server just in case).
Success! After the installation, we can update our
create_user/1-function in the
Accounts module to take advantage of the Comeonin library. We want to extract the user's password from the parameters that are sent in, encrypt it, and then put it in the changeset using
While we're in this file, we're also going to add some more constraints to the changeset function so that we can give clearer error messages to the user when something is wrong:
These functions should be quite easy to understand even if they're new.
unique_constraint ensures that there are no duplicates of a particular field (since we can't have 2 users with the same username for instance) while
validate_format makes sure that a field matches a certain regex (for the email, we just check if there's an
validate_length in combination with
min: 4 makes sure that the field is at least 4 characters long.
Now, we can finally create our first real user! If you want, you can delete the user that we created previously in the database, so that you can get a fresh start! I'm going to type in
tristan as my username,
email@example.com as my email, and
password as my password. Feel free to change these values.
Updating the login page
So we've completed the signup part. Now we also need to let the user log in, and stay logged in.
We'll start with the easy part of just creating the necessary routes, templates and controller functions. You should be pretty familiar with this process now, since it's very similar to the one we had for the signup functionality.
Let's tweak our login template so that it has a dynamic form that's similar to the one on our signup page. The difference is that we won't use a changeset for this one, so we need to specify the key under which all the input values are going to be sent under (in this case, we'll choose
Based on this, you should see what our next steps should be. We need to create the
login_user/2-function in our PageController. For now,
login_user/2 won't do anything except show an error flash if the user tries to sign in.
And finally, no route is complete without an entry in the
router.ex-file! Let's specify the path as
POST /login, and add it right under
Creating a Session
So how do we check if the given username and password is correct? Well, the Comeonin library has a handy
checkpw/2-function that helps us compare an encrypted password with a plain text password! Easy peasy!
Before implementing this though, we should take a step back and reflect about how many steps are involved in this login process. It seems like we would have to perform at least the following steps:
Get the user's form parameters
Read the username and find the matching user from the database
Compare the matching user's encrypted password to the supplied password
If it matches, create a session so that the logged-in user persists
Hm, that seems like quite a lot of functionality! Let's create a new
Session module in our "accounts" context to handle some of this, so that we have some clear separation of concerns. This is how we envision it to work in our controller afterwards:
Let's start coding that new module! The first thing we'll do is alias the stuff we know we'll need and create an empty
We now want to get the username and password from the credentials and fetch the user from the database that matches that username. This can easily be done with some clever pattern matching and one line of code:
Now that we have the user, we want to compare this user's
encrypted_password to the
given_password. For that we'll create a
Note that we've defined the function
The first one is called in case
user turns out to be
Repo.get_by will return
nil if it can't find a user that matches the given username).
The second one uses pattern matching to get the
encrypted_password from the user struct. We then compare that password to the one given by the user thanks to Comeonin's
In both cases, we either return a tuple with the
:ok atom and the user struct, or a tuple with
:error and an error message.
This is good enough for now! Let's go back to our Page Controller and use this module. This basically mirrors the functionality of
create_user – we always render the
login.html template, but based on the return value of
Session.authenticate, we either add an info flash or an error flash:
Nothing spooky here, just some classic pattern matching. You should now be able to see the different messages when you play around with the login page!
Persisting the user with Guardian
Even though our flash message says "Logged in as tristan", we haven't actually written any code to persist the user yet. All we've done so far is verify that the login credentials were valid, but there's nothing keeping us logged in yet. Thankfully, the rest is pretty easy thanks to the awesome Guardian library.
Let's add Guardian as a dependency in our
mix.exs file, like we did with Comeonin. Until Guardian is updated (see issue), we also need to add
override:true on the
If we follow Guardian's installation instructions, you'll also see that we need to set some configuration options. We can do this at the very bottom of
Please note that our current
secret_key ("5ecret_k3y") is terribly insecure. In order to generate one that's actually secure for production usage, you can use the Mix task
As you can also see from the code, our configuration requires that we have a module called
Messengyr.Auth.Guardian. We'll just copy the one that Guardian provides in their documentation and create a file for it in our "accounts"-context:
Alright, stop your server and run
mix deps.get to install and recompile. Now you can run
mix phx.server again, and we're ready to use Guardian in our app!
Saving the logged-in user to the current session is as easy as adding a line to the
login_user/2-function in our PageController! We simply use Guardian's special
sign_in/2 function like this:
If we log in again now, our user will be saved to the session (although you won't see it)! The final step is to fetch the info from this session on every request so that we can display the user, because there's no point in logging in if everything still looks the same right?
Fetching the session info
To tell Phoenix that we want to fetch the session info on every HTTP request, we need to go back to the Router file and create a special pipeline.
We'll call this pipeline
browser_session, and all it does is use two Guardian plugs – VerifySession (which checks if we're logged in), and LoadResource (which checks who exactly is logged in).
Following Guardian's documentation, we create a special pipeline module:
Then we use it in a brand new
browser_session pipeline which we put underneath the existing
And then we make sure that we use this pipeline in our default scope:
This should be enough to make it work! Let's test it out by experimenting with the landing page. We'll fetch the user with Guardian's
current_resource function and log it in the
index function of the Page Controller.
Reload the landing page (
/) in your browser and you should see this in your console, provided that you logged in before:
We now have evidence that our signup and authentication works. The next step is to improve the UI of our Phoenix app to take it from being merely a website, to a full-blown web app!