Table of Contents

Introduction
Getting Started
Installing and Configuring Dependencies
Creating the User Model
Creating the User Migration
Creating the Tokens Migration
Running the Migrations
Setting Up the Auth Routes
Creating the User Controller
Creating the Auth View
Creating the Auth Controller
Test Drive
Accessing Protected Routes

Introduction

Gotta have users. Well, at least you’d probably like to have some users if you’re building a web or mobile app. And let’s get real; what the hell would you be doing on this page if you’re not currently building or looking to build a web or mobile app?

We all know authentication and authorization are kind of a turd to get moving. So let’s stir the pot and drop that plunger once and for all.

Getting Started

Alright, let’s get going. We’re going to be building a JSON API backend that will be able to serve whatever client you’d like, whether it’s a single-page web application or a native mobile app. We’re also going to be using MySQL for my development environment because I’ve lost enough days of my life trying to install PostgreSQL on my machine and I’ll spare you that crisis. Pop open a terminal and initialize your application:

$ mix pheonix.new MyPhoenixBackend --no-brunch --no-html --database mysql

Once that’s finished, configure your database credentials in config/dev.exs then run the following in your terminal:

$ mix ecto.create

Installing and Configuring Dependencies

Overview

Comeonin

We’ll be using Comeonin to hash the raw passwords provided by users when they sign up.

Guardian

We’ll be using Guardian to check every request (after a successful login) and make sure the user is authenticated/authorized. It’s important to keep in mind that Guardian does not have anything to do with the initial challenge of validating a user’s email and password. That’s up to you and Üeberauth. Once your user’s credentials are validated, Guardian will help create the token and check it on every subsequent request.

GuardianDb

We’ll be using GuardianDb as an additional layer on top of Guardian to store tokens in the database so we can verify that they’re still valid. Standard/pure JWT authentication/authorization does not persist state in the database. This sounds like a great idea and one that minimizes database queries, but it has some very serious negative consequences. Don’t even get me started.

Installation

What we should get started on is adding all these dependencies. Open up your project’s mix.exs file and add everything to your deps and application blocks. They should end up looking something like this:

# mix.exs

...

def application do
  [mod: {MyPhoenixBackend, []},
   applications: [:phoenix, :phoenix_pubsub, :cowboy, :logger, :gettext,
                  :phoenix_ecto, :mariaex, :comeonin, :guardian, :guardian_db]]
end

...

defp deps do
  [
    {:phoenix, "~> 1.2.1"},
    {:phoenix_pubsub, "~> 1.0"},
    {:phoenix_ecto, "~> 3.0"},
    {:mariaex, "~> 0.8.1", override: true},
    {:gettext, "~> 0.11"},
    {:cowboy, "~> 1.0"},
    {:comeonin, "~> 3.0"},
    {:guardian, "~> 0.14"},
    {:guaridan_db, "~> 0.7.0"}
  ]
end

...

In your terminal, run the following to fetch all the dependencies and compile your application:

$ mix deps.get, compile

Configuration

Now it’s time to configure those new dependencies. Open up config/config.exs and make sure it looks something like this:

# config/config.exs

...

config :guardian, Guardian,
  hooks: GuardianDb,
  issuer: "MyPhoenixBackend",
  ttl: { 365, :days },
  allowed_drift: 2000,
  secret_key: to_string(Mix.env) <> "SoMe LoNg HaShy StRinG tHaT yOu CaN mAkE uP 1234567890!@#$%^&*()",
  serializer: MyPhoenixBackend.GuardianSerializer

config :guardian_db, GuardianDb,
       repo: MyPhoenixBackend.Repo

...

The Guardian Serializer

You’ll need to provide a custom serializer for Guardian to do it’s job. First, create a directory called “token” inside of the “web” directory. Then create a file inside that new directory called guardian_serializer.ex. Open it up in your text editor and add the following code. Feel free to tweak it any way you see fit:

# web/token/guardian_serializer.ex
defmodule MyPhoenixBackend.GuardianSerializer do
  @behaviour Guardian.Serializer

  alias MyPhoenixBackend.Repo
  alias MyPhoenixBackend.User

  def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
  def for_token(_), do: { :error, "Unknown resource type" }

  def from_token("User:" <> id), do: { :ok, Repo.get(User, id) }
  def from_token(_), do: { :error, "Unknown resource type" }
end

Creating the User Model

Run the following in your terminal:

$ mix phoenix.gen.json User users first_name:string email:string hashed_password:string

Here we’ll set up our User model to do a number of things. Feel free to skip the explanation. I do the same thing (“Give me the damn code!”). But here’s the skinny:

  1. First we’ll add a virtual :password field to the User model. It’s virtual because it’s not a field in the database users table and we won’t be storing it. It’s sensitive, okay?

  2. Then we’ll create a private helper function called put_password_hash that will hash the provided password before we store it in the database just in case Sneaky Pete comes poking around.

  3. In order for that cool put_password_hash function to work, we’ll need to lean on the Comeonin module we installed as a dependency. So we’ll alias that for extreme convenience.

  4. Finally, we’ll add a changeset that we’ll name registration_changeset. This changeset will be used when we’re creating new users. It’ll do a handful of things including requiring specific attributes, checking that the password and password_confirmation are equal, validating the format and uniqueness of the user’s email and length of their password, as well as applying the put_password_hash function we’ll be adding. Now, when you think “signing up” for a site/app, think creating a new user. Don’t confuse this with “signing in” to a site/app. We’ll get to that later but it has no business here. Signing up for an app will not in itself create a new authentication token. It simply creates a new user with verifiable credentials (like email and password) that will be checked later when logging in. If you deem it an appropriate user experience you can of course log your user in immediately after creating the account. Totally fine. But the concerns should be separate. Often sites will require a user to verify their account via an email sent to the new registree. In this case you don’t want to log your user in directly following the initial sign up. We’re not going to get that gritty here but the point remains that signing up and signing in are two very different things.

All in all, here’s what this little doosie should look like:

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

  alias Comeonin.Bcrypt

  schema "users" do
    field :first_name, :string
    field :email, :string
    field :hashed_password, :string
    field :password, :string, virtual: true

    timestamps()
  end

  def registration_changeset(user, params \\ :empty) do
    user
    |> cast(params, [:first_name, :email, :password])
    |> validate_required([:first_name, :email, :password])
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 6)
    |> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
    |> unique_constraint(:email)
    |> put_password_hash
  end

  defp put_password_hash(changeset) do
    case changeset do
      %Ecto.Changeset{ valid?: true, changes: %{ password: pass } } ->
        put_change(changeset, :hashed_password, Comeonin.Bcrypt.hashpwsalt(pass))
      _ ->
        changeset
    end
  end
end

Creating the User Migration

The User migration file will already have been generated when you ran the mix phoenix.gen.json command earlier in the terminal. We just want to add a unique index on the email field and not-null requirements on all fields. Here’s what the migration should look like:

# priv/repo/migrations/*_create_user.exs
defmodule MyPhoenixBackend.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :first_name, :string, null: false
      add :email, :string, null: false
      add :hashed_password, :string, null: false

      timestamps()
    end

    create unique_index(:users, [ :email ])
  end
end

Creating the Tokens Migration

As of early 2017, the documentation for Üeberauth’s GuardianDb requires the addition of a table name guardian_tokens. You can configure GuardianDb to use a different table name if you’d like. This is necessary to keep track of valid tokens. (Really, don’t get me started on using pure JWT for sessions. Sure, it would be fantastic if we could have a truly stateless server that also didn’t need to make a database query to authorize. But what if an account is compromised? Or even more simply, what if a user decides to stop being a paying customer? What do you do with those tokens then? Just wait for them to expire while that user still has full access to restricted portions of the site/app because their tokens are still totally valid in the eyes of the token-signing algorithm? I don’t think so. Or on the flip side, have token expiration timeframes be so short that your users constantly have to keep signing in? I don’t think so. Well – you say – you could keep a blacklist of invalid tokens, which by its very nature destroys the idea of pure JWT auth. Sure you could do that. Or you could keep a whitelist. I’m glass half full on this one.)

At any rate, let’s get that migration going. In the terminal:

$ mix ecto.gen.migration create_guardian_tokens

and in that freshly-minted migration file:

# priv/repo/migrations/*_create_guardian_tokens.exs
defmodule MyPhoenixBackend.Repo.Migrations.CreateGuardianTokens do
  use Ecto.Migration

  def change do
    create table(:guardian_tokens, primary_key: false) do
      add :jti, :string, primary_key: true
      add :aud, :string, primary_key: true
      add :typ, :string
      add :iss, :string
      add :sub, :string
      add :exp, :bigint
      add :jwt, :text
      add :claims, :map

      timestamps
    end
  end
end

Running the Migrations

In your terminal, run:

$ mix ecto.migrate

Setting Up the Auth Routes

# web/router.ex
defmodule MyPhoenixBackend.Router do
  use MyPhoenixBackend.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :bearer_auth do
    plug Guardian.Plug.VerifyHeader, realm: "Bearer"
    plug Guardian.Plug.LoadResource
  end

  pipeline :ensure_auth do
    plug Guardian.Plug.EnsureAuthenticated
  end

  scope "/auth", MyPhoenixBackend do
    pipe_through :api

    post "/sign-in", AuthController, :sign_in
    post "/sign-up", UserController, :create
  end

  scope "/api", MyPhoenixBackend do
    pipe_through [ :api, :bearer_auth, :ensure_auth ]

    resources "/lists", ListController, except: [ :new, :edit ]
  end
end

Creating the User Controller

Alright, we’re finally at the fun part! We’re going to build out the User controller to allow users to sign up for our site/app. Signing up for an app equates to inserting a new record in the users table, which is why we’ve routed /auth/sign-up to the create function of the User controller.

We already have the web/controllers/user_controller.ex file from when we ran mix phoenix.gen.json User... before. Open it up in your text editor and edit the create function to look like the following:

# web/controllers/user_controller.ex

...

def create(conn, %{"user" => user_params}) do
  changeset = User.registration_changeset(%User{}, user_params)

  case Repo.insert(changeset) do
    {:ok, user} ->
      conn
      |> put_status(:created)
      |> render("show.json", user: user)
    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(MyPhoenixBackend.ChangesetView, "error.json", changeset: changeset)
  end
end

...

The real workhorse here is the registration_changeset that performs all the validations and hashes the raw password. This leaves the controller quite simple and readable.


You can now officially sign users up for your site/app! If you’d like to see this in action, start the Phoenix server from your terminal:

$ mix phoenix.server

Now, send a POST request to http://localhost:4000/auth/sign-up. You can do this on the command-line with curl or with the super awesome and free tool Postman. Make sure you set the “Content-Type” header to “application/json” and format the body of your request to look something like this:

{
  "user": {
    "first_name": "Kyle",
    "email": "kyle@mail.com",
    "password": "123456",
    "password_confirmation": "123456"
  }
}

If everything has been plumbed successfully, you should get a response that looks like the following:

{
  "data": {
    "id": 42,
    "first_name": "Kyle",
    "email": "kyle@mail.com"
  }
}

Creating the Auth View

Now that we’ve handled signing up, let’s tackle signing in. We’ll need to create two new files for this: web/controllers/auth_controller.ex and web/views/auth_view.ex. Let’s start with the view:

# web/views/auth_view.ex
defmodule MyPhoenixBackend.AuthView do
  use MyPhoenixBackend.Web, :view

  def render("show.json", %{ token: token, user_id: user_id }) do
    %{
      token: token,
      user_id: user_id
    }
  end

  def render("401.json", %{ message: message }) do
    %{
      errors: [
        %{
          id: "UNAUTHORIZED",
          title: "401 Unauthorized",
          detail: message,
          status: 401
        }
      ]
    }
  end

  def render("403.json", %{ message: message }) do
    %{
      errors: [
        %{
          id: "FORBIDDEN",
          title: "403 Forbidden",
          detail: message,
          status: 403
        }
      ]
    }
  end

  def render("delete.json", _) do
    %{ ok: true }
  end
end

This view file is nothing more than a handful of functions your controller will invoke in order to send JSON back to the client. The main purpose here is simply formatting the response.

Creating the Auth Controller

Now open up the controller in your text editor and make it look like the following:

# web/controllers/auth_controller.ex
defmodule MyPhoenixBackend.AuthController do
  use MyPhoenixBackend.Web, :controller

  import Comeonin.Bcrypt, only: [ checkpw: 2, dummy_checkpw: 0 ]

  alias MyPhoenixBackend.User

  def sign_in(conn, params = %{ "email" => _, "password" => _ }) do
    case check_email_and_password(params) do
      { :ok, user } ->
        case Guardian.encode_and_sign(user) do
          {:ok, jwt, _claims} ->
            conn
            |> put_status(:created)
            |> render("show.json", %{ token: jwt, user_id: user.id})
          {:error, :token_storage_failure} ->
            handle_unauthenticated(conn, "There was an error creating the session (:token_storage_failure)")
          {:error, reason} ->
            handle_unauthenticated(conn, reason)
        end
      { :error, reason } ->
        handle_unauthenticated(conn, reason)
    end
  end

  defp handle_unauthenticated(conn, reason) do
    conn
    |> put_status(:unauthorized)
    |> render("401.json", message: reason)
  end

  defp check_email_and_password(%{ "email" => email, "password" => password }) do
    user = User |> Repo.get_by(email: email)

    cond do
      user && checkpw(password, user.hashed_password) ->
        { :ok, user }
      user ->
        { :error, "The provided password doesn't match the provided email (#{email})" }
      true ->
        dummy_checkpw()
        { :error, "User does not exist with email #{email}" }
    end
  end
end

This is easily the most complicated piece of logic we’ve seen so far. But really it just looks like a lot. Let’s break it down:

The sign_in function checks that the email and password provided by the user are valid (by invoking the check_email_and_password function), and either proceeds with the signing in or responds to the client that the login info is invalid. If the email/password combo is valid, then it uses Guardian to sign the user in. Under the hood, when the Guardian.encode_and_sign function is called, Guardian and GuardianDb create a JWT and insert a corresponding record in the guardian_tokens table. Then it returns a tuple with the status of the operation: { :ok, user } if everything went well and { :error, ... } if there was a problem. Based on this tuple, we then respond to the client accordingly.

That’s the long and the short of the sign in process.

For a bit more detail, the check_email_and_password function first queries the users table to find the user with the provided email. Then it runs through a cond statement, which will evaluate the first block that follows a truthy statement. The first statement (user && checkpw(password, user.hashed_password)) will be truthy if there exists a user in the users table with an email that matches the one provided by the client and if the password provided matches the stored hashed_password. In order to make this comparison, we use the Comeonin.Bcrypt library and invoke checkpw. This will hash the newly provided password and check it against the stored hashed_password. The second statement (user) will be truthy if there exists a user in the users table with an email that matches the one provided by the client but the password does not match the one stored in hashed_password. The final statement (true) acts as a catch-all and will be called when neither the provided email nor password are valid.

The handle_unauthenticated function simply orchestrates a response back to the client that the request is unauthorized when something goes wrong.

And that’s it!

Test Drive

We can now sign up and sign in. Let’s do just that. Above, we sent a POST request to the /auth/sign-up endpoint to register a user. If you haven’t done so already, go ahead and register a user.

Now, send a POST request (remembering to set the “Content-Type” header to “application/json”) to /auth/sign-in with a request body that looks like this:

{
  "email": "kyle@mail.com",
  "password": "123456"
}

If everything goes according to plan, you should receive a response similar to this:

{
  "user_id": 42,
  "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJVc2VyOjQxIiwiZXhwIjoxNTE3MDY3MDMwLCJpYXQiOjE0ODU1MzEwMzAsImlzcyI6Ik15UGhvZW5peEJhY2tlbmQiLCJqdGkiOiIzYWVlZWE2OC1jMTg1LTRkMTgtYjY4YS1mZmRmOWVlOWVjYTIiLCJwZW0iOnt9LCJzdWIiOiJVc2VyOjQxIiwidHlwIjoiYWNjZXNzIn0.s8vzqiKoEHAQw0_Fl7t4kuDSRbdgTAPiKt2B7zKcKntKieA3msyNlF1YKovO-WkD4KUFfOwui2E9nKHNV6lr0Q"
}

It’s now up to the client to store that token. In a browser-based app you can save it in a cookie, session storage, local storage, or any other way you’d like. You can even just keep it in memory of you want your users logged out immediately upon leaving your site.

Personally, I’m an Ember guy, so I lean towards Ember Simple Auth to easily handle the client’s portion of auth responsibility in a web app.

On the mobile app side of things, I can only point you to a NativeScript solution since that’s how I build native mobile apps: Application Settings. Here, onec you receive the token from your Phoenix backend, you’d store it in the “application-setting”.

Regardless of how you store the token, we’ll need to set it in the “Authorization” header of all subsequent requests we send to our Phoenix API. Specifically, here’s what the header should look like: Bearer: eyJhbGciOiJIUz.... Note that we need to concatenate “Bearer: ” to the beginning of the token string.

Accessing Protected Routes

Now that we have our sign-up and sign-in systems in place, let’s build a resource that’s only accessible to authenticated users. In your terminal, run the following:

$ mix phoenix.gen.json List lists title:string user_id:references:users
Advertisements