Discover Elixir & Phoenix

Back to All Courses

Lesson 11

Authentication

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 uplog 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

defmodule Messengyr.Mixfile do
  
  # ...
  
  defp deps do
    [
      {:phoenix, "~> 1.3.0-rc"},
      # ...
      {:cowboy, "~> 1.0"},
      {:comeonin, "~> 3.0"} # Add this row!
    ]
  end

  # ...
  
end

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 do deps.get, compile

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

defmodule Messengyr.Accounts do

  # ...

  # Pattern match to extract the password
  def create_user(%{"password" => password} = params) do
    
    # Encrypt the password with Comeonin:
    encrypted_password = Comeonin.Bcrypt.hashpwsalt(password)

    register_changeset(params)
    |> put_change(:encrypted_password, encrypted_password)
    |> Repo.insert
  end

  # ...

end

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

defmodule Messengyr.Accounts do

  # ...

  def register_changeset(params \\ %{}) do
    %User{}
    |> cast(params, [:username, :email, :password])
    |> validate_required([:username, :email, :password])
    |> unique_constraint(:email)
    |> unique_constraint(:username)
    |> validate_format(:email, ~r/@/)
    |> validate_format(:username, ~r/^[a-zA-Z0-9]*$/)
    |> validate_length(:password, min: 4)
  end

end

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 routestemplates 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

<div class="card login">

  <h1>Log in to Messenger</h1>

  <%= form_for @conn, page_path(@conn, :login_user), [as: :credentials], fn f -> %>

    <%= text_input f, :username, placeholder: "Username" %>

    <%= text_input f, :password, placeholder: "Password", type: "password" %>

    <%= submit "Log in", class: "login" %>

  <% end %>

</div>

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

defmodule MessengyrWeb.PageController do
  
  # ...

  def login_user(conn, params) do
    conn
    |> put_flash(:error, "Unable to log in!")
    |> render("login.html")
  end
  
end

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

defmodule MessengyrWeb.Router do
  
  # ...

  scope "/", MessengyrWeb do
    # ...

    post "/signup", PageController, :create_user
    post "/login", PageController, :login_user # Add this line!
  end

  # ...
  
end

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

# ...

  def login_user(conn, %{"credentials" => credentials}) do
    case Session.authenticate(credentials) do
      {:ok, user} ->
        # We're logged in!

      {:error, message} ->
        # Oh noez! Something went wrong!
    end
  end

  # ...

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

defmodule Messengyr.Accounts.Session do

  alias Messengyr.Accounts.User
  alias Messengyr.Repo

  def authenticate(credentials) do
  	# We will add some code here soon.
  end

end

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

defmodule Messengyr.Accounts.Session do

  # ...

  def authenticate(%{"username" => username, "password" => given_password}) do
    user = Repo.get_by(User, username: username)
  end

  # ...

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

defmodule Messengyr.Accounts.Session do

  alias Messengyr.Accounts.User
  alias Messengyr.Repo

  def authenticate(%{"username" => username, "password" => given_password}) do
    user = Repo.get_by(User, username: username)

    # Call the function here:
    check_password(user, given_password)
  end

  # In case no user was found before:
  defp check_password(nil, _given_password) do
    {:error, "No user with this username was found!"}
  end

  # Use Comeonin to compare the passwords:
  defp check_password(%{encrypted_password: encrypted_password} = user, given_password) do
    case Comeonin.Bcrypt.checkpw(given_password, encrypted_password) do
      true -> {:ok, user}
      _    -> {:error, "Incorrect password"}
    end
  end

end

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

defmodule MessengyrWeb.PageController do
  
  # ...
  
  # Don't forget to alias the Session module!
  alias Messengyr.Accounts.Session
  
  # ...

  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
  
end

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

defmodule Messengyr.Mixfile do
  
  # ...
  
  defp deps do
    [
      # ...
      {:comeonin, "~> 3.0"},
      {:guardian, "~> 1.0"} # Add this line!
    ]
  end

  # ...
  
end

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

# ...

# Configures the Guardian library
config :messengyr, Messengyr.Auth.Guardian,
  issuer: "Messengyr",
  ttl: { 30, :days },
  allowed_drift: 2000,
  secret_key: "5ecret_k3y",

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

defmodule Messengyr.Auth.Guardian do
  use Guardian, otp_app: :messengyr

  alias Messengyr.Repo
  alias Messengyr.Accounts.User

  def subject_for_token(user = %User{}, _claims) do
    { :ok, to_string(user.id) }
  end

  def subject_for_token(_, _) do
    { :error, "Unknown resource type" }
  end

  def resource_from_claims(claims) do
    { :ok, Repo.get(User, claims["sub"]) }
  end

end

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

defmodule MessengyrWeb.PageController do
  
  # ...
  
  alias Messengyr.Accounts.Guardian # <-- Make sure you alias "Guardian"
  
  # ...

  def login_user(conn, %{"credentials" => credentials}) do
    case Session.authenticate(credentials) do
      {:ok, %{username: username} = user} -> # Extract the "user"...
        conn
        |> Guardian.Plug.sign_in(user) # ...and add this line!
        |> put_flash(:info, "Logged in as #{username}!")
        |> render("login.html")

      # ...
      
    end
  end
  
end

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

defmodule Messengyr.Auth.Pipeline do
  use Guardian.Plug.Pipeline,
    otp_app: :messengyr,
    module: Messengyr.Auth.Guardian

  plug Guardian.Plug.VerifySession
  plug Guardian.Plug.LoadResource, allow_blank: true
end 

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

# lib/messengyr_web/router.ex

defmodule MessengyrWeb.Router do
  use MessengyrWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  # Add this pipeline:
  pipeline :browser_session do
    plug Messengyr.Auth.Pipeline
  end

  # ...
  
end

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

# lib/messengyr_web/router.ex

defmodule MessengyrWeb.Router do
  
  # ...

  scope "/", MessengyrWeb do
    pipe_through [:browser, :browser_session] # Add "browser_session"

    get "/", PageController, :index
    
    # ...
  end

  # ...
  
end

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

defmodule MessengyrWeb.PageController do
  
  # ...

  def index(conn, _params) do
    # Add these 2 lines:
    user = Guardian.Plug.current_resource(conn)
    IO.inspect user

    render conn
  end

  # ...
  
end

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!