Index

The App

I wanted to make an application that was simple enough that I could focus on implementing Authentication and Authorization, and complex enough to have a semi-realistic Authentication scenario. In keeping with tradition (and because I intend to do another more detailed tutorial in the future) we will be building a blog (hurray!… well, ground work or basis for a blog), but we will have user management restricted to an Admin user. So, the only routes that will be public will be our homepage, sign up, and register. The rest will need authentication, and listing as well as deleting users will require role-based authorization.

Phauxth - First things first

The first package we will use is Phauxth. It should be quite clear I consider this the best package for database authentication from my last post, and I think you will see why shortly. In order to get started we must install it. So open up a terminal and run the command mix archive.install https://github.com/riverrun/phauxth/raw/master/installer/archives/phauxth_new.ez to download the cli.

Starting our app

I will be using Phoenix 1.3 (and you should be too), if you have not yet used 1.3, or if you don’t fully understand contexts yet, then feel free to set up an app context to avoid thinking about it for now.

First we’ll create a new app mix phx.new blog_phx then we cd into the directory and run mix phauxth.new . There are some options we can pass, however, I will leave that to you to read about - you can see the documentation here: https://github.com/riverrun/phauxth/wiki/Getting-started . This will generate a User schema in an accounts context, we could use our own user schema, but I like putting User in an Accounts context anyway so I’ll let Phauxth do the writing for me. There is one thing I want to do before we create the database and run the migrations though. If we open the file blog_phx/priv/repo/<date_stamp>_create_users.exs, we should see a migration as follows:

defmodule BlogPhx.Repo.Migrations.CreateUsers do
  use Ecto.Migration

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

      timestamps()
    end

    create unique_index :users, [:email]
  end
end

Make these changes:

defmodule BlogPhx.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string
      add :password_hash, :string
      add :role, :string    #add this line

      timestamps()
    end

    create unique_index :users, [:email]
  end
end

And in the schema file at blog_phx/lib/blog_phx/accounts/user.ex :

defmodule BlogPhx.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias BlogPhx.Accounts.User

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :password_hash, :string
    field :role, :string, default: "user"   #Add this line

    timestamps()
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:email, :password, :role]) #Add :role here
    |> validate_required([:email])
    |> unique_constraint(:email)
  end

  def create_changeset(%User{} = user, attrs) do
    user
    |> changeset(attrs)
    |> validate_password(:password)
    |> put_pass_hash()
  end

  # In the function below, strong_password? just checks that the password
  # is at least 8 characters long.
  # See the documentation for NotQwerty123.PasswordStrength.strong_password?
  # for a more comprehensive password strength checker.
  def validate_password(changeset, field, options \\ []) do
    validate_change(changeset, field, fn _, password ->
      case strong_password?(password) do
        {:ok, _} -> []
        {:error, msg} -> [{field, options[:message] || msg}]
      end
    end)
  end

  # If you are using Argon2 or Pbkdf2, change Bcrypt to Argon2 or Pbkdf2
  def put_pass_hash(%Ecto.Changeset{valid?: true, changes:
      %{password: password}} = changeset) do
    change(changeset, Comeonin.Bcrypt.add_hash(password))
  end
  def put_pass_hash(changeset), do: changeset

  defp strong_password?(password) when byte_size(password) > 7 do
    {:ok, password}
  end
  defp strong_password?(_), do: {:error, "The password is too short"}
end

So what did we do? We just added a role field to our schema and table that allows use to do role based authorization later on.

This line:

field :role, :string, default: "user"

Tells phoenix there is a field called role, if it is not specified, then default to “user”.

There is one more change I’d like to make to our seeds file. Phauxth is thoughtful enough to set up some users for us to use in our new app. I would like to add an admin user as well.

Our seed file changes (blog_phx/priv/repo/seeds.exs):

users = [
  %{email: "jane.doe@example.com", password: "password"},
  %{email: "john.smith@example.org", password: "password"},
  %{email: "admin@admin.com", password: "adminpassword", role: "admin"} #Add this line
]

for user <- users do
  {:ok, _} = BlogPhx.Accounts.create_user(user)
end

And the last change we have to make is to add our dependencies. We need the Phauxth dependency, however, we can choose to use Bcrypt, Argon2, or Pbkdf2 for our encryption strategy. Bcrypt will be fine for our needs, however if you want to change this you just need to add it as a dependency and make a few changes in user.ex and session_controller.ex.

in our blog_phx/mix.exs file, make these changes:

  defp deps do
    [
      {:phoenix, "~> 1.3.0"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:phauxth, "~> 1.0"},   #Add this dependency
      {:bcrypt_elixir, "~> 1.0"}   #Add this dependency
    ]
  end

Note: bcrypt_elixir 1.0 is for erlang 20 and up, consult docs.

lastly running mix deps.get and mix deps.compile to bring in and build our dependencies.

We can now run the migrations and create the database with either: mix ecto.create and then mix ecto.migrate and then running our seeds file, OR, simply: mix ecto.setup to do all the above (Obviously make sure you have configured your database first).

You can now iex -S mix into the application and run BlogPhx.Accounts.list_users to see all the users the seed file created. Almost there!

What is going on?

So we pretty much set up our authentication, there are a few more things to know about how Phauxth does authentication that I will go over quickly. If you want to dig deeper, the best thing about OSS is that you can peek into the code (blog_phx/deps/phauxth/lib/authenticate/base.ex is a good place to start). Phauxth allows you to extend their base module giving you a great number of possibilities for customization that is straight forward and well thought out & structured.

In order to give you a quick overview of what is happening, let’s take a gander (yes, I just busted out gander. 20 hipster points) at our router.ex file.

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

  scope "/", BlogPhxWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
    resources "/sessions", SessionController, only: [:new, :create, :delete]
  end

You can see every route that is piped through :browser is going through our authentication is run through the authentication plug, however, if you start up the app you will notice you can access the PageController index route, and the SessionController routes. This is because restricting access is authorization (which we will cover shortly) NOT authentication. To better get an understanding about what is going on, let’s open up our PageController and do some debugging.

blog_phx/lib/blog_phx_web/controllers/page_controllers.ex:

  def index(conn, _params) do
    render conn, "index.html"
    require IEx   #Add this
    IEx.pry       #Add this
  end

Now start the server in an iex session - iex -S mix phx.server and visit localhost:4000 in a browser, returning to your terminal you should see a prompt asking if you want to run pry type ‘y’ for yes. Once at the prompt type in conn to see the current Plug.Conn struct. Near the top you should see a value that says current_user: nil. If you do the same procedure in an app without Phauxth that will not be there. Basically this means we are authenticated as unknown (you can extend this to be guest if you want a more rigid role-based authorization). So we have been authenticated, just not as anyone in particular. We can add a current user value by authenticating (logging in) with one of the credentials we put in our seeds file. Play around with logging in with a user and admin. You will notice no matter the user, we can delete other users! What a security risk! Let’s fix that (make sure you delete the IEx lines we added before moving forward).

Authorization

So we learned that all our routes are being authenticated, but why can we access the login, register, and home route, and not the user routes? Let’s find out.

Open up blog_phx/lib/blog_phx_web/controllers/user_controller.ex:

  # the following plugs are defined in the controllers/authorize.ex file
  plug :user_check when action in [:index, :show]
  plug :id_check when action in [:edit, :update, :delete]

These two lines are interesting, they are even documented! Basically, it is saying these actions (index, show) must have a current_user, while these actions (edit, update, delete) must have a current_user and that current_user’s id must correlate to the user action they are requesting. Essentially, you can only delete, edit, and update your own account, but can see a listing of all user accounts as well as individual accounts (I hope that is clear). But we don’t want that for our app. Only an admin should be able to do everything (including delete - joining our app is a big commitment!) and the user should be able to edit and view his own account. There are only a few changes we have to make to achieve this, but first let’s see these plugs user_check and id_check.

Open up blog_phx/lib/blog_phx_web/controllers/authorize.ex:

  def user_check(%Plug.Conn{assigns: %{current_user: nil}} = conn, _opts) do
    error(conn, "You need to log in to view this page", session_path(conn, :new))
  end
  def user_check(conn, _opts), do: conn

  def id_check(%Plug.Conn{assigns: %{current_user: nil}} = conn, _opts) do
    error(conn, "You need to log in to view this page", session_path(conn, :new))
  end
  def id_check(%Plug.Conn{params: %{"id" => id},
      assigns: %{current_user: current_user}} = conn, _opts) do
    if id == to_string(current_user.id) do
      conn
    else
      error(conn, "You are not authorized to view this page", user_path(conn, :index))
    end
  end

I strongly suggest you take a moment to understand everything that is happening in this file, as it will provide you with a strong basis in regards to authorization and extending Phauxth.

Had a bit of a read? Got everything down pat? Great, we just need to adda plug to provide rolebased authentication. In their documentation they actually provide this as a demo for extending authorization, so let’s grab that!

    def role_check(%Plug.Conn{assigns: %{current_user: nil}} = conn, _opts) do
    error conn, "You need to log in to view this page", session_path(conn, :new)
    end
    def role_check(%Plug.Conn{assigns: %{current_user: current_user}} = conn, opts) do
    if opts[:roles] && current_user.role in opts[:roles], do: conn,
    else: error conn, "You are not authorized to view this page", page_path(conn, :index)
    end

I made a few changes to get it working for now however it still won’t work right for our needs, but we’ll get back to that shortly. for now let’s go back to our user_controller and make a few changes.

Delete these lines:

  # the following plugs are defined in the controllers/authorize.ex file
  plug :user_check when action in [:index, :show]
  plug :id_check when action in [:edit, :update, :delete]

And replace with this:

  plug :role_check, [roles: ["admin"]]

Now you will notice when a user with the role “user” signs in, they can no longer access the user routes (not even their own account!), however, an user with the role “admin” can access everything. What I would probably do in a real app is create a seperate route for a profile view, and have a controller with only the user show and update features with an id_check plug, however, to save time, and to show how we can pinpoint specific actions in controllers, we will create another plug instead.

Add this to authorize.ex:

  def id_or_role(%Plug.Conn{assigns: %{current_user: nil}} = conn, _opts), do: error(conn, "You need to log in to view this page", session_path(conn, :new))
  def id_or_role(%Plug.Conn{params: %{"id" => id}, assigns: %{current_user: current_user}} = conn, opts) do
    if opts[:roles] && current_user.role in opts[:roles] or id == to_string(current_user.id) do
      conn
    else
      error(conn, "You are not authorized to view this page", page_path(conn, :index))
    end
  end

And replace the auth plug part in our user controller:

  plug :role_check, [roles: ["admin"]] when action in [:index, :delete]
  plug :id_or_role, [roles: ["admin"]] when action in [:show, :update, :edit]

I realize that if you wanted the code to be a bit more coupled you could just have made a plug called something like id_or_admin and checked specifically for “admin” role and not had to supply a list of roles, but I like to keep my code a bit more flexible.

You should now be able to access your specific profile as a user, and everything as an admin. You may notice there are a few bugs, let’s quickly sort them out.

In our session controller blog_phx/lib/blog_phx_web/controllers/session_controller.ex, we are currently being redirected to the users page that we don’t have access to:

  def create(conn, %{"session" => params}) do
    case Login.verify(params, BlogPhx.Accounts) do
      {:ok, user} ->
        put_session(conn, :user_id, user.id)
        |> configure_session(renew: true)
        |> success("You have been logged in", page_path(conn, :index)) #make change from user to page_path
      {:error, message} ->
        error(conn, message, session_path(conn, :new))
    end
  end

We could have checked if the user was an admin and choose the route based on that, but it is getting late in Medellin, so I’ll leave you to tweak how you like; however, there is one change I’d like to show you.

Once logged in we see the users navigation link at the top no matter what level of authorization we have and that bugs me.

blog_phx/lib/blog_phx_web/templates/layout/app.html.eex:

    <%= if @current_user do %>
	<%= if @current_user.role == "admin" do %>
	    <li><a href="<%= user_path @conn, :index %>">Users</a></li>
	<% end %>
    <li><a href="<%= user_path(@conn, :show, @current_user.id) %>"><%= @current_user.email %></a></li>
    <li><%= link "Logout", to: session_path(@conn, :delete, @current_user), method: :delete %></li>
    <% else %>
    <li><a href="<%= user_path @conn, :new %>">New user</a></li>
    <li><a href="<%= session_path @conn, :new %>">Login</a></li>
    <% end %>

If we wrap the link to the user_path in a quick check for the admin role we can achieve the result we want. I would refactor this out, do the check in the controller and pass that on, but as I said, it is late.

Done and Done

I think despite the simplicity of our app, one can easily see how you could extend it to include varying degrees of checks/plugs in a control for whatever conditions your app requires. It should also be clear how concise and easy to follow this package is, and how it gets out of our way and allows us to focus on bigger problems. Again, I really like this package, but I will have Guardian and Coherence demos coming up so that you can choose what works best for you and your app.

Cheers!