Roll your own User Authentication with Phoenix

Phoenix Cookbook : part 4 of 4 published on Jun 19, 2015

There are some libraries that are in the early stages for user authentication with Phoenix, but I wouldn’t say there is a clear winner like devise was for Rails.

But have no fear, it is super easy to roll your own user authentication for your Phoenix application.

Add the comeonin library

For the password encryption and checking, we will be utilizing the comeonin elixir library: https://github.com/elixircnx/comeonin

To add the comeonin library to our application add the following to our mix.exs file.

{:comeonin, "~> 1.0"}

And install the package using mix.

$ mix deps.get

Create the User model

First we will create and fill out a barebones migration.

$ mix ecto.gen.migration create_users
# /priv/repo/migrations/TIMESTAMP_create_users.exs
defmodule UserAuthExample.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string
      add :password_hash :string

      timestamps
    end
  end
end

Now make our User model.

# /web/models/user.ex
defmodule UserAuthExample.User do
  use UserAuthExample.Web, :model

  schema "users" do
    field :email, :string
    field :password_hash, :string
    timestamps
  end

end

Register a User and Encrypt Password

I break up my application into Commands and Queries. Commands generally being Use Case specific actions. In this case, registering a user.

We will create a user and encrypt their password with comeonin‘s hashpwsalt/1 function.

# /web/commands/register_user.ex
defmodule UserAuthExample.RegisterUser do
  import Comeonin.Bcrypt
  alias UserAuthExample.User
  alias UserAuthExample.Repo

  def execute(email, password) do
    user = %User{
      email: email,
      password_hash: hashpwsalt(password)
    } |> Repo.insert

    { :ok, user }
  end

end

Here is a test verifying it works.

# /test/commands/register_user/ok_test.exs
defmodule UserAuthExample.RegisterUser.OkTest do
  use UserAuthExample.ModelCase
  alias UserAuthExample.User
  alias UserAuthExample.RegisterUser

  test "user is created and password hashed" do
    result = RegisterUser.execute("tony@stark.com", "iamironman")

    { :ok, user } = result
    
    assert user.id != nil
    assert user.email == "tony@stark.com"
    assert user.password_hash != nil
    assert user.password_hash != "iamironman"

    assert Enum.count(User |> Repo.all) == 1
  end

end

Authenticate User

To authenticate a user we will make another command and call it AuthenticateUser. Inside the function we will use comeonin‘s checkpw/2 function to verify the password.

# /web/commands/authenticate_user.ex
defmodule UserAuthExample.AuthenticateUser do
  import Comeonin.Bcrypt
  import Ecto.Query
  alias UserAuthExample.User
  alias UserAuthExample.Repo

  def execute(email, password) do
    user = User
      |> where([u], u.email == ^email)
      |> Repo.one

    validate_password(user, password)
  end

  # if we don't find a user
  def validate_password(nil, _), do: invalidate_credentials

  # if the password is blank
  def validate_password(_, nil), do: invalidate_credentials

  # check password against password hash
  def validate_password(user, password) do
    result = checkpw(password, user.password_hash)
    case result do
      false -> invalidate_credentials
      true -> { :ok, user }
    end
  end

  def invalidate_credentials, do: 
    { :error, "Incorrect email and/or password" }
  
end

For a passing test:

# /test/commands/authenticate_user/ok_test.exs
defmodule UserAuthExample.AuthenticateUser.OkTest do
  use UserAuthExample.ModelCase
  alias UserAuthExample.RegisterUser
  alias UserAuthExample.AuthenticateUser
  
  test "return ok and user to sign in" do
    { :ok, user } = RegisterUser.execute("tony@stark.com", "iamironman")
    
    result = AuthenticateUser.execute("tony@stark.com", "iamironman")
    
    assert result == { :ok, user }
  end

end

And our failing tests:

# /test/commands/authenticate_user/no_user_test.exs
defmodule UserAuthExample.AuthenticateUser.NoUserTest do
  use UserAuthExample.ModelCase
  alias UserAuthExample.AuthenticateUser
  
  test "return error" do
    result = AuthenticateUser.execute("loki@badguys.com", "imthebest")
    
    assert result == { :error, "Incorrect email and/or password" }
  end

end
# /test/commands/authenticate_user/missing_password_test.exs
defmodule UserAuthExample.AuthenticateUser.MissingPasswordTest do
  use UserAuthExample.ModelCase
  alias UserAuthExample.RegisterUser
  alias UserAuthExample.AuthenticateUser
  
  test "return error" do
    { :ok, user } = RegisterUser.execute("tony@stark.com", "iamironman")
    
    result = AuthenticateUser.execute("tony@stark.com", nil)
    
    assert result == { :error, "Incorrect email and/or password" }
  end

end
# /test/commands/authenticate_user/bad_password_test.exs
defmodule UserAuthExample.AuthenticateUser.BadPasswordTest do
  use UserAuthExample.ModelCase
  alias UserAuthExample.RegisterUser
  alias UserAuthExample.AuthenticateUser
  
  test "return error" do
    { :ok, user } = RegisterUser.execute("tony@stark.com", "iamironman")
    
    result = AuthenticateUser.execute("tony@stark.com", "captamericasucks")
    
    assert result == { :error, "Incorrect email and/or password" }
  end

end

Require Authenticated User

Now since we have command functions to register and sign in a user, we need to create a Plug so that we can require a user to sign in for specific routes.

The plug will check for the current user in the session and if not found redirect to a sign in page.

# /web/plugs/require_authentication.ex

defmodule UserAuthExample.Plugs.RequireAuthentication do
  import Plug.Conn
  import Phoenix.Controller

  def init(options) do
    options
  end

  def call(conn, _) do
    user = get_session(conn, :current_user)
    case user do
      nil ->
        conn |> redirect(to: "/sign_in") |> halt
      _ ->
        conn |> assign(:current_user, user)
    end
  end

end

We can use this plug on a controller:

defmodule UserAuthExample.SomeController do
  use UserAuthExample.Web, :controller
  
  # be sure to be this before the :action plug
  plug UserAuthExample.Plugs.RequireAuthentication
  plug :action
  ...
  
end

Or as part of a pipeline:

pipeline :require_user do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug UserAuthExample.Plugs.RequireAuthentication
end

Sign in

For simplicity sake, we are just going to show the minimal bits (yet still try to be thorough).

If the user is successfully authentication, we set the :current_user with the authetnicated user record.

# /web/router.ex
...
get "/sign_in", SessionsController, :new
post "/sign_in", SessionsController, :create
...
# /web/controllers/sessions_controller.ex
defmodule UserAuthExample.SessionsController do
  use UserAuthExample.Web, :controller

  plug :action

  def new(conn, _params) do
    render conn, "new.html"
  end

  def create(conn, %{ "sign_in" => %{ "email" => email, "password" => password } }) do
    result = UserAuthExample.AuthenticateUser.execute(email, password)

    case result do
      { :ok, user } ->
        conn
        |> put_session(:current_user, user)
        |> redirect(to: "/")
      { :error, message } ->
        conn
        |> put_flash(:error, message)
        |> render "new.html"
    end
  end
end
# /web/templates/sessions/new.html.eex
<%= form_for @conn, "/sign_in", [name: :sign_in], fn f -> %>
  <div class="form-group">
    <label>Email</label>
    <%= text_input f, :email, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= password_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

Sign out

And to sign out, we just need to clear out :current_user from the session.

# /web/router.ex
...
delete "/sign_in", SessionsController, :destroy
...
# /web/controllers/sessions_controller.ex
...
  def destroy(conn, _params) do
    conn 
    |> put_session(:current_user, nil)
    |> redirect(to: "/sign_in")
  end
...
# any view
<%= form_for @conn, "/sign_in", [name: :sign_in, method: :delete], fn f -> %>
  <div class="form-group">
    <%= submit "Sign out", class: "btn btn-primary" %>
  </div>
<% end %>

Review

This is a clearly a simple example and has several areas to improve. But the main parts of user authentication are present.

We can…

  1. register a user and generate a password hash
  2. authenticate a user and password against the password hash
  3. require a user be authenticated to have access to given routes
  4. sign in a user
  5. sign a user out

Eventually a devise-like library will win out in popularity and usage. Until then though, rolling your own authentication isn’t hard and can be quite educational. :)