Lesson 11

Authentication

7

In this lesson, we're going to use two great Elixir libraries, Comeonin and Guardian, to make sure that the users of our app can sign up, log in and maintain a session.

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:

mix.exs

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).


Mix installing Comeonin.


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 Ecto.Changeset's put_change:

lib/messengyr/accounts/accounts.ex

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:

lib/messengyr/accounts/accounts.ex

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 @-sign). validate_length in combination with min: 4 makes sure that the field is at least 4 characters long.


You can experiment with different combinations of invalid values, and you'll see different error messages on the page!


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, tristan@ludu.co as my email, andpassword as my password. Feel free to change these values.


Type in the info you want...



...and boom! Seems like the info flash also works as expected!



It might also be good to double-check that the user was indeed created in the database!


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 :credentials):

lib/messengyr_web/templates/page/login.html.eex

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.

lib/messengyr_web/controllers/page_controller.ex

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 POST /signup:

lib/messengyr_web/router.ex


You should now be able to go to /login, click the button, and see the error message.


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:

  1. Get the user's form parameters

  2. Read the username and find the matching user from the database

  3. Compare the matching user's encrypted password to the supplied password

  4. 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:

lib/messengyr_web/controllers/page_controller.ex

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 authenticate/1-function:

lib/messengyr/accounts/session.ex

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:

lib/messengyr/accounts/session.ex

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 check_password/2-function:

lib/messengyr/accounts/session.ex

Note that we've defined the function check_password/2 twice.


The first one is called in case user turns out to be nil (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 checkpw/2 function.


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:

lib/messengyr_web/controllers/page_controller.ex

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 phoenix dependency:

mix.exs

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 config/config.exs:

config/config.exs

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 mix guardian.gen.secret.


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:

lib/messengyr/auth/guardian.ex

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:

lib/messengyr_web/controllers/page_controller.ex

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:

lib/messengyr/auth/pipeline.ex

Then we use it in a brand new browser_session pipeline which we put underneath the existing browser pipeline:

lib/messengyr_web/router.ex

And then we make sure that we use this pipeline in our default scope:

lib/messengyr_web/router.ex

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.

lib/messengyr_web/controllers/page_controller.ex

Reload the landing page (/) in your browser and you should see this in your console, provided that you logged in before:


Woohoo! There's our user!


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!

Comments

profile/avatar/default
Egoleo

I am getting the below error with your code.


[error] #PID<0.551.0> running MessengyrWeb.Endpoint (connection #PID<0.543.0>, stream id 4) terminated

Server: localhost:4000 (http)

Request: POST /login

** (exit) an exception was raised:

** (ArgumentError) Comeonin.Bcrypt.checkpw has been removed.

Add {:bcrypt_elixir, "~> 2.0"} to the deps in your mix.exs file,

and use Bcrypt.verify_pass instead.


So i tried below code but still getting issues with it. 


Any ideas?


profile/avatar/default
Egoleo

WHat is message in this function?


def login_user(conn, %{"credentials" => credentials}) do

case Session.authenticate(credentials) do

{:ok, %{username: username}} ->

conn

|> put_flash(:info, "Logged in as #{username}!")

|> render("login.html")

{:error, message} ->

conn

|> put_flash(:error, message)

|> render("login.html")

end

end

Tristan Edwards

message is the error message that gets thrown.

profile/avatar/default
Egoleo

ok but is it supposed to as a variable? Because this is the error i am getting.


warning: variable "message" does not exist and is being expanded to "message()", please use parentheses to remove the ambiguity or change the variable name

lib/messengyr_web/controllers/page_controller.ex:53: MessengyrWeb.PageController.login_user/2

profile/avatar/default
Bret Savage

mix phoenix.gen.secret is deprecated. Use phx.gen.secret instead.

profile/avatar/default
Bret Savage

Or guardian.gen.secret

Tristan Edwards

@5avage: Thanks for the heads up. I've updated the lesson!

profile/avatar/default
Jeffylube

I also needed to include {bcrypt_elixir, "~&gt; 1.0"}