Phoenix: simple authentication & authorization in step-by-step tutorial form

Andrey Chernykh
24 min readAug 4, 2016

--

In this tutorial that I think is for beginner Elixir/Phoenix developers, I’ll try to explain how can you use Phoenix pipelines and plugs in pair with awesome Guardian library in order to implement basic authentication and authorization. Of course, for authorization needs you can utilize another great library — Canary, but, sometimes, it will be an over-engineering. Moreover, it will be helpful to understand how useful Phoenix piplines and plugs are.

We’re going to create simple User-Post project as base for our experiments. We’ll call it just SimpleAuth.

Start new project, go to our brand new project’s directory and create a database (as Phoenix mix task will propose):

mix phoenix.new simple_auth
mix ecto.create

Let’s start with User model and make use of Phoenix generator for it:

mix phoenix.gen.model User users email:string name:string password_hash:string is_admin:boolean

Generated migration should look like this:

defmodule SimpleAuth.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string
add :name, :string
add :password_hash, :string
add :is_admin, :boolean, default: false, null: false
timestamps
end
end
end

Let’s edit the migration a bit: add unique index in order to allow only unique email be stored in the database, and add null: false constraint to :email field:

defmodule SimpleAuth.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string, null: false
add :name, :string
add :password_hash, :string
add :is_admin, :boolean, default: false, null: false
timestamps
end
create unique_index(:users, [:email])
end
end

Also, we need Posts table referenced to Users. Let’s generate that model:

mix phoenix.gen.model Post posts title:string body:text user_id:references:users

And we’ve got generated migration:

defmodule SimpleAuth.Repo.Migrations.CreatePost do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string
add :body, :text
add :user_id, references(:users, on_delete: :nothing)
timestamps
end

create index(:posts, [:user_id])
end
end

We will not change CreatePost migration, so run those two migrations:

mix ecto.migrate

Now we have our project’s database configured. Open the user model’s file and add virtual :password field to the schema:

# web/models/user.ex# ...
schema "users" do
field :email, :string
field :name, :string
field :password, :string, virtual: true
field :password_hash, :string
field :is_admin, :boolean, default: false
timestamps
end
# ...

Virtual :password field will exist in Ecto structure but not in the database, so we are able to provide password to the model’s changesets and, therefore, validate that field.

Also, add has_many :posts association to the schema, that will serve navigation purposes:

has_many :posts, SimpleAuth.Post

Thanks to Phoenix generator, we already have belongs_to :user association in Post model schema:

belongs_to :user, SimpleAuth.User

Define required and optional params as module attributes in both models. This will save our time during changesets implementation and refactoring.

# web/models/user.ex# ...
@required_fields ~w(email)a
@optional_fields ~w(name is_admin)a
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end
# ...

I’ve also added association constraint on :user field into Post model’s default changeset. It ensures that referenced user exists in the database before a post saving.

# web/models/post.ex# ...
@required_fields ~w(title)a
@optional_fields ~w(body)a
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> assoc_constraint(:user)
end
# ...

From now on we’re ready to implement user registration and authentication.

Let’s start with user CRUD, where creation is considered as registration, obviously.

Personally, I don’t like scaffold generators and think it would be better to follow the tutorial’s narration if we will not use them.

Let’s create SimpleAuth.UserController module with show and new actions implementation:

# web/controllers/user_controller.exdefmodule SimpleAuth.UserController do
use SimpleAuth.Web, :controller
alias SimpleAuth.User def show(conn, %{"id" => id}) do
user = Repo.get!(User, id)
render(conn, "show.html", user: user)
end
def new(conn, _params) do
changeset = User.changeset(%User{})
render conn, "new.html", changeset: changeset
end
def create(conn, %{"user" => user_params}) do
# here will be an implementation
end
end

And add user resource to the router:

# web/router.ex# ...
scope "/", SimpleAuth do
pipe_through :browser
get "/", PageController, :index resources "/users", UserController, only: [:show, :new, :create]
end
# ...

In most cases and, particularly, in this tutorial a user is not able to see users listing nor delete himself, so we specified we’re interested only in :show, :new and :create routes.

Next, create a simple SimpleAuth.UserView:

# web/views/user_view.exdefmodule SimpleAuth.UserView do
use SimpleAuth.Web, :view
end

and basic templates for our actions in new web/templates/user directory:

# web/templates/user/show.html.eex<h2><%= @user.name %></h2><p><%= @user.email %></p># web/templates/user/new.html.eex<h1>User Registration</h1><%= form_for @changeset, user_path(@conn, :create), fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>There are some errors</p>
</div>
<% end %>
<div class="form-group">
<%= text_input f, :email, placeholder: "Email",
class: "form-control" %>
<%= error_tag f, :email %>
</div>
<div class="form-group">
<%= text_input f, :name, placeholder: "Name",
class: "form-control" %>
<%= error_tag f, :name %>
</div>
<div class="form-group">
<%= password_input f, :password, placeholder: "Password",
class: "form-control" %>
<%= error_tag f, :password %>
</div>
<%= submit "Create User", class: "btn btn-primary" %>
<% end %>

Also, update our main layout template in order to display handy registration link:

# web/templates/layout/app.html.eex# ...
<header class="header">
<nav role="navigation">
<ul class="nav nav-pills pull-right">
<li>
<%= link "Register", to: user_path(@conn, :new) %>
</li>
</ul>
</nav>
<span class="logo"></span>
</header>
# ...

It seems like we set up all required parts responsible for browser request handling.

Run your local Phoenix server:

mix phoenix.server

And check your link and form are displayed and firstly implemented SimpleAuth.UserController’s actions work properly.

Now we need to implement SimpleAuth.UserController’s create action. But before we start we should take care about our users password storing. As you might noticed, we’ve defined :password_hash for that purpose. As the next step we’ll implement a changeset for user registration and hashing function, which will be utilized by this changeset. For hashing password we will use comeonin library.

Add comeonin to /mix.exs file (into deps and application functions, btw: there is the great article by João Britto, explained what, where, when and why we should define in that mixfile functions):

# /mix.exs# ...
def application do
[mod: {SimpleAuth, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :comeonin]]
end
# ...defp deps do
[{:phoenix, "~> 1.2.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.5"}]
end
# ...

Then call mix deps.get to retrieve new dependency.

Now proceed to new registration changeset implementation:

# web/models/user.ex# ...
def registration_changeset(struct, params) do
struct
|> changeset(params)
|> cast(params, ~w(password)a, [])
|> validate_length(:password, min: 6, max: 100)
|> hash_password
end
defp hash_password(changeset) do
case changeset do
%Ecto.Changeset{valid?: true,
changes: %{password: password}} ->
put_change(changeset,
:password_hash,
Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
# ...

Here we passed our user’s struct through default changeset firstly, then cast :password parameter, validate its length and invoke hash_password/1 function. hash_password/1 function checks if changeset is valid and changes in user’s struct contains :password parameter (the field is going to be changed). We hash given :password parameter with comeonin library if all checks are successful, and return untouched changeset otherwise.

Now implement :create action of our SimpleAuth.UserController (do not forget to call :scrub_params function plug in order to convert blank-string params into nils):

# web/controllers/user_controller.ex# ...
plug :scrub_params, "user" when action in [:create]
# ...
def create(conn, %{"user" => user_params}) do
changeset = %User{} |> User.registration_changeset(user_params)
case Repo.insert(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "#{user.name} created!")
|> redirect(to: user_path(conn, :show, user))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
#...

Return to the browser, go to /users/new page, fill up the form and press ‘Create User’ and you should see something like this:

Now, let’s check our :password_hash field stores password hash:

iex(1)> alias SimpleAuth.Repo
SimpleAuth.Repo
iex(2)> alias SimpleAuth.User
SimpleAuth.User
iex(3)> u = User |> Repo.one
[debug] QUERY OK db=1.5ms
SELECT u0.”id”, u0.”email”, u0.”name”, u0.”password_hash”, u0.”is_admin”, u0.”inserted_at”, u0.”updated_at” FROM “users” AS u0 []
%SimpleAuth.User{__meta__: #Ecto.Schema.Metadata<:loaded, “users”>,
email: “test@test.com”, id: 1,
inserted_at: #Ecto.DateTime<2016–08–01 13:46:46>, is_admin: false,
name: “Test User”, password: nil,
password_hash: “$2b$12$0v812htTOeWD7WonQjWYmu9wRDrVQmgIijMNOFSFI5ALxPt5p1LSG”,
posts: #Ecto.Association.NotLoaded<association :posts is not loaded>,
updated_at: #Ecto.DateTime<2016–08–01 13:46:46>}

Great. So far, so good. From now on we can create/register a user. As the next step, we need to deal with authentication, session’s creation and deleting etc.

Let’s start our authentication implementation from routes. Add /sessions resource to the router module:

# web/router.ex# ...
scope "/", SimpleAuth do
pipe_through :browser
get "/", PageController, :index resources "/users", UserController, only: [:show, :new, :create] resources "/sessions", SessionController, only: [:new, :create,
:delete]
end
# ...

Obviously, we only need listed actions: :new for login form displaying; :create for authentication implementation, based on provided parameters; :delete for logout.

Next, create SimpleAuth.SessionsController with new action implementation and create/delete actions blueprints:

# web/controllers/session_controller.exdefmodule SimpleAuth.SessionController do
use SimpleAuth.Web, :controller
plug :scrub_params, "session" when action in ~w(create)a def new(conn, _) do
render conn, "new.html"
end
def create(conn, %{"session" => %{"email" => email,
"password" => password}}) do
# here will be an implementation
end
def delete(conn, _) do
# here will be an implementation
end
end

And proper view and template as we did before with users:

# web/views/session_view.exdefmodule SimpleAuth.SessionView do
use SimpleAuth.Web, :view
end

# web/templates/session/new.html.eex
<h1>Sign in</h1><%= form_for @conn, session_path(@conn, :create),
[as: :session], fn f -> %>
<div class="form-group">
<%= text_input f, :email, placeholder: "Email",
class: "form-control" %>
</div>
<div class="form-group">
<%= password_input f, :password, placeholder: "Password",
class: "form-control" %>
</div>
<%= submit "Sign in", class: "btn btn-primary" %>
<% end %>

As you noticed, session form declaration differs from user form a bit: since we don’t have session resource (model) we can’t infer form parameters from the changeset. Here we provide connection and declare [as: :session] instead (in order to get params[“session”] in controller actions).

Also we added link ‘Sign in’ just aside our ‘Register’ link in app.html.eex layout:

# web/templates/layout/app.html.eex# ...
<ul class="nav nav-pills pull-right">
<li>
<%= link "Register", to: user_path(@conn, :new) %>
</li>
<li>
<%= link "Sign in", to: session_path(@conn, :new) %>
</li>
</ul>
# ...

Here how should the result of that code looks like:

Now, we have SimpleAuth.SessionController blueprint and the form ready to send session parameters to the controller’s create action. Let’s implement it.

But before we should add another dependency to our project — Guardian, authentication framework, which will handle all the stuff related to tokens, session storage, necessary plugs etc. Guardian is really powerful, please, find time to check its documentation and examples to learn more about its features. In our project we will utilize only the base functionality.

Add Guardian as a dependency to the mixfile:

{:guardian, "~> 0.12.0"}

Get it (mix deps.get) and configure in config.exs file, our simple configuration might look like this:

# config/config.exs# ...
config :guardian, Guardian,
issuer: "SimpleAuth.#{Mix.env}",
ttl: {30, :days},
verify_issuer: true,
serializer: SimpleAuth.GuardianSerializer,
secret_key: to_string(Mix.env) <> "SuPerseCret_aBraCadabrA"
# ...

Notice, that we should provide serializer module that is used for a subject’s serialization/deserialization (it will be our user) from/into token. There a simple serializer realisation below. Merely from Guardian’s documentation. We’ll place it to brand new web/auth directory:

# web/auth/guardian_serializer.exdefmodule SimpleAuth.GuardianSerializer do
@behaviour Guardian.Serializer
alias SimpleAuth.Repo
alias SimpleAuth.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

Now we have simply configured Guardian as dependency and can move further.

We had switched to adding Guardian just before SimpleAuth.SessionController’s create action implementation. This is how we can code it:

# web/controllers/session_controller.eximport Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]alias SimpleAuth.User# ...
def create(conn, %{"session" => %{"email" => email,
"password" => password}}) do

# try to get user by unique email from DB
user = Repo.get_by(User, email: email)
# examine the result
result = cond do
# if user was found and provided password hash equals to stored
# hash
user && checkpw(password, user.password_hash) ->
{:ok, login(conn, user)}
# else if we just found the use
user ->
{:error, :unauthorized, conn}
# otherwise
true ->
# simulate check password hash timing
dummy_checkpw
{:error, :not_found, conn}
end
case result do
{:ok, conn} ->
conn
|> put_flash(:info, "You’re now logged in!")
|> redirect(to: page_path(conn, :index))
{:error, _reason, conn} ->
conn
|> put_flash(:error, "Invalid email/password combination")
|> render("new.html")
end
end
defp login(conn, user) do
conn
|> Guardian.Plug.sign_in(user)
end
# ...

There is a lot of code that I covered with comments.

Now go to http://localhost:4000/sessions/new, submit signin form with user credentials you created earlier and you can see fresh session token stored in browser cookies:

Definitely, it would be helpful to be able to get a signed in user within our application’s code. Guardian provides such access with the current_resource/1 function:

Guardian.Plug.current_resource(conn)

But, in my opinion, it’s not so handy to invoke this function from templates, for instance, every time you need current user. It is much nicer to have current_user variable in our templates. To do so, we can create a simple plug:

# web/auth/current_user.exdefmodule SimpleAuth.CurrentUser do
import Plug.Conn
import Guardian.Plug
def init(opts), do: opts def call(conn, _opts) do
current_user = current_resource(conn)
assign(conn, :current_user, current_user)
end
end

This plug just gets current_resource from a Guardian token (in our case — user) and assigns it as property to our connection. So, now we can access signed in user through @current_user variable (connection assignment) in our templates (or controllers, or whatever).

In order to have SimpleAuth.CurrentUser plug working, we need somehow integrate it in our project. The best place for it — router piplines. You might be tempted to add it into default :browser pipline, but, I suggest to group plugs logically, so we’ll define new pipeline:

# web/router.ex# ...
pipeline :with_session do
plug Guardian.Plug.VerifySession
plug Guardian.Plug.LoadResource
plug SimpleAuth.CurrentUser
end
# ...

I’ve called it :with_session (you can call it as you like, though) and added a bunch of Guardian plugs. VerifySession plug checks for a token existence in a session, LoadResource plug fetches a value from token’s sub field and serializes it with the serializer module we’ve created earlier (it makes Guardian.Plug.current_resource(conn) function works).

Now add :with_session pipeline into main scope’s pipelines listing:

pipe_through [:browser, :with_session]

Nice. Next, let’s implement session deletion (signing out) and, therefore, delete action of SimpleAuth.SessionController:

# web/controllers/session_controller.ex# ...
def delete(conn, _) do
conn
|> logout
|> put_flash(:info, "See you later!")
|> redirect(to: page_path(conn, :index))
end
defp logout(conn) do
Guardian.Plug.sign_out(conn)
end
# ...

Armed with @current_user variable, we can do tiny refactoring of our links group in the main layout:

# web/templates/layout/app.html.eex<ul class="nav nav-pills pull-right">
<%= if @current_user do %>
<li><%= @current_user.email %> (<%= @current_user.id %>)</li>
<li>
<%= link "Sign out", to: session_path(@conn, :delete,
@current_user),
method: "delete" %>
</li>
<% else %>
<li><%= link "Register", to: user_path(@conn, :new) %></li>
<li><%= link "Sign in", to: session_path(@conn, :new) %></li>
<% end %>
</ul>

Restart your Phoenix server and go to http://localhost:4000, try out signin/signout functionality:

It works! What can we do else? Well, it would be great to sign in a new user after registration. Let’s review SimpleAuth.UserController’s create action:

# web/controllers/user_controller.ex# ...
def create(conn, %{"user" => user_params}) do
changeset = %User{} |> User.registration_changeset(user_params)
case Repo.insert(changeset) do
{:ok, user} ->
conn
|> put_flash(:info, "#{user.name} created!")
|> redirect(to: user_path(conn, :show, user))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
# ...

Definitely, we should invoke login/2 function as we did in SimpleAuth.SessionController’s create action. But it leads us to some code duplication. I propose to move auth-related functionality to a separate module and refactor SimpleAuth.SessionController:

# web/auth/auth.exdefmodule SimpleAuth.Auth do
import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]
alias SimpleAuth.User
alias SimpleAuth.Repo
def login(conn, user) do
conn
|> Guardian.Plug.sign_in(user)
end
def login_by_email_and_pass(conn, email, given_pass) do
user = Repo.get_by(User, email: email)
cond do
user && checkpw(given_pass, user.password_hash) ->
{:ok, login(conn, user)}
user ->
{:error, :unauthorized, conn}
true ->
dummy_checkpw
{:error, :not_found, conn}
end
end
def logout(conn) do
Guardian.Plug.sign_out(conn)
end
end
# web/controllers/session_controller.exdefmodule SimpleAuth.SessionController do
use SimpleAuth.Web, :controller
plug :scrub_params, "session" when action in ~w(create)a def new(conn, _) do
render conn, "new.html"
end
def create(conn, %{"session" => %{"email" => email,
"password" => password}}) do
case SimpleAuth.Auth.login_by_email_and_pass(conn, email,
password) do
{:ok, conn} ->
conn
|> put_flash(:info, "You’re now signed in!")
|> redirect(to: page_path(conn, :index))
{:error, _reason, conn} ->
conn
|> put_flash(:error, "Invalid email/password combination")
|> render("new.html")
end
end
def delete(conn, _) do
conn
|> SimpleAuth.Auth.logout
|> put_flash(:info, "See you later!")
|> redirect(to: page_path(conn, :index))
end
end

Nothing either new or special in the code above, we’ve just decoupled common auth-related functions and the controller. From that moment, we can invoke SimpleAuth.Auth module functions from any controller/test/helper we want. Let’s return to signing in after a user successful creation:

# web/controllers/user_controller.ex# ...
def create(conn, %{"user" => user_params}) do
changeset = %User{} |> User.registration_changeset(user_params)
case Repo.insert(changeset) do
{:ok, user} ->
conn
|> SimpleAuth.Auth.login(user)
|> put_flash(:info, "#{user.name} created!")
|> redirect(to: user_path(conn, :show, user))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
# ...

In the code above we just added SimpleAuth.Auth.login(user) into successful pipe. Simple enough :) Let’s check it out.

If we successfully create a user that user immediately will be signed in. Sweet!

So far we’ve written a lot of code. All that code was related to user registration and authentication. Next, we are going to implement user posts CRUD and authorization rules.

There are our project’s authorization rules:

  • every registered user is able to read any other user’s post
  • every registered user can retrieve any other user’s posts list
  • guests (unregistered users) can not read any post
  • administrator is similar to registered user in terms of posts reading

Although, admin users are exceptional. They can delete any post of any user and see users and posts lists in special admin zone.

In order to visualize those authorization rules we will start from the router. Add two new pipelines blueprints and add two new scopes:

# web/router.ex# ...
pipeline :login_required do
end
pipeline :admin_required do
end
# guest zone
scope "/", SimpleAuth do
pipe_through [:browser, :with_session]
get "/", PageController, :index resources "/sessions", SessionController, only: [:new, :create,
:delete]
resources "/users", UserController, only: [:new, :create] # registered user zone
scope "/" do
pipe_through [:login_required]
resources "/users", UserController, only: [:show] do
resources "/posts", PostController
end
# admin zone
scope "/admin", Admin, as: :admin do
pipe_through [:admin_required]
resources "/users", UserController, only: [:index, :show] do
resources "/posts", PostController, only: [:index, :show]
end
end
end
end
# ...

Looking at the router’s scopes we can imagine which controllers and actions we need and how we should handle authorization restrictions over requests. It’s worth to notice how Phoenix routing is elegant, at least in pipelines & scopes nesting :)

Let’s start implement SimpleAuth.PostController:

# web/controllers/post_controller.exdefmodule SimpleAuth.PostController do
use SimpleAuth.Web, :controller
alias SimpleAuth.Post
alias SimpleAuth.User
plug :scrub_params, "post" when action in [:create, :update] def action(conn, _) do
apply(__MODULE__, action_name(conn),
[conn, conn.params, conn.assigns.current_user])
end
def index(conn, _params, current_user) do
# here will be an implementation
end
def show(conn, %{"id" => id}, current_user) do
# here will be an implementation
end
def new(conn, _params, current_user) do
# here will be an implementation
end
def create(conn, %{"post" => post_params}, current_user) do
# here will be an implementation
end
def edit(conn, %{"id" => id}, current_user) do
# here will be an implementation
end
def update(conn, %{"id" => id, "post" => post_params},
current_user) do
# here will be an implementation
end
def delete(conn, %{"id" => id}, current_user) do
# here will be an implementation
end
defp user_posts(user) do
assoc(user, :posts)
end
defp user_post_by_id(user, post_id) do
user
|> user_posts
|> Repo.get(post_id)
end
end

In the controller blueprint I omitted actions implementation for now, because I want to make an emphasis on other functions.

user_posts/1 and user_post_by_id/2 functions are just helpers, which will reduce code duplication in our actions.

As you may noticed, there is additional argument passed to the controller actions — well-known current_user. Obviously, current user information plays important role in handling authorization issues, that’s why we’re passing this parameter around.

More interestingly how we’ve provided it to each action in the controller. As you guess action/2 is responsible for this. This function is invoked before any action in a controller. Within it’s body we simply call (by apply/3 function) a controller action with the list of arguments (by default: conn and conn.params), which we extended with current user stored in connection structure (conn.assigns.current_current_user). (Remember how we’ve put current user there with our SimpleAuth.CurrentUser plug?)

Ok, now each and every action in SimpleAuth.PostController has an access to current user without a need to fetch :current_user from conn.assigns map every time we need it.

Let’s implement index and show actions as the next step. Here the full implementation:

# web/controllers/post_controller.ex# ...
def index(conn, %{"user_id" => user_id}, _current_user) do
user = User |> Repo.get!(user_id)
posts =
user
|> user_posts
|> Repo.all
|> Repo.preload(:user)
render(conn, "index.html", posts: posts, user: user)
end
def show(conn, %{"user_id" => user_id, "id" => id},
_current_user) do
user = User |> Repo.get!(user_id)
post = user |> user_post_by_id(id) |> Repo.preload(:user) render(conn, "show.html", post: post, user: user)
end
# ...

The implementation is pretty simple and similar to SimpleAuth.UserController. Except, we should fetch posts related to user (fetched by user_id parameter) from the database.

Also, don’t forget to create the view and templates:

# web/views/post_view.exdefmodule SimpleAuth.PostView do
use SimpleAuth.Web, :view
end
# web/templates/post/index.html.eex<%= if @current_user.id == @user.id do %>
<h1>My Posts</h1>
<%= link "Create new", to: user_post_path(@conn, :new,
@current_user) %>
<% else %>
<h1><%= @user.name %>'s Posts</h1>
<% end %>
<ul>
<%= for post <- @posts do %>
<li>
<%= link post.title, to: user_post_path(@conn, :show,
post.user, post) %>
</li>
<% end %>
</ul>
# web/templates/post/show.html.eex<%= link "To list", to: user_post_path(@conn, :index, @post.user) %><h1><%= @post.title %></h1><p><%= @post.body %></p><%= if @current_user.id == @user.id do %>
<span>
<%= link "Edit", to: user_post_path(@conn, :edit, @current_user,
@post), class: "btn btn-primary btn-xs" %>
&nbsp;
<%= link "Delete", to: user_post_path(@conn, :delete,
@current_user, @post),
method: :delete,
data: [confirm: "Really?"],
class: "btn btn-danger btn-xs"%>
</span>
<% end %>

As you noticed, I’ve made control buttons been depended on simple user checks: only post owner can edit/delete the post.

Change a bit the main layout header as well:

# web/templates/layout/app.html.eex# ...
<div class="container">
<div class="header">
<ol class="breadcrumb text-right">
<%= if @current_user do %>
<li>
<%= @current_user.email %> (<%= @current_user.id %>)
</li>
<li>
<%= link "My posts", to: user_post_path(@conn, :index,
@current_user) %>
</li>
<li>
<%= link "Sign out", to: session_path(@conn, :delete,
@current_user), method: "delete" %>
</li>
<% else %>
<li><%= link "Register", to: user_path(@conn, :new) %></li>
<li><%= link "Sign in", to: session_path(@conn, :new) %></li>
<% end %>
</ol>
</div>
</div>
# ...

Let’s continue to implement SimpleAuth.PostController’s actions and make control buttons do some useful stuff.

# web/controllers/post_controller.ex# ...
def new(conn, _params, current_user) do
changeset =
current_user
|> build_assoc(:posts)
|> Post.changeset
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"post" => post_params}, current_user) do
changeset =
current_user
|> build_assoc(:posts)
|> Post.changeset(post_params)
case Repo.insert(changeset) do
{:ok, _} ->
conn
|> put_flash(:info, "Post was created successfully")
|> redirect(to: user_post_path(conn, :index, current_user.id))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def edit(conn, %{"id" => id}, current_user) do
post = current_user |> user_post_by_id(id)
changeset = Post.changeset(post) render(conn, "edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params},
current_user) do
post = current_user |> user_post_by_id(id)
changeset = Post.changeset(post, post_params) case Repo.update(changeset) do
{:ok, _} ->
conn
|> put_flash(:info, "Post was updated successfully")
|> redirect(to: user_post_path(conn, :show, current_user.id,
post.id))
{:error, changeset} ->
render(conn, "edit.html", post: post, changeset: changeset)
end
end
def delete(conn, %{"id" => id}, current_user) do
current_user |> user_post_by_id(id) |> Repo.delete!
conn
|> put_flash(:info, "Post was deleted successfully")
|> redirect(to: user_post_path(conn, :index, current_user.id))
end
# ...

In the actions above we are interested in creating, updating and deleting only on owned posts, thus, we have not to deal with user with id from URI and don’t pattern match on user_id parameter.

Create the rest of templates:

# web/templates/post/new.html.eex<h1>Create Post</h1><%= form_for @changeset, user_post_path(@conn, :create, @current_user),
fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>There are some errors</p>
</div>
<% end %>
<div class="form-group">
<%= text_input f, :title, placeholder: "Title",
class: "form-control" %>
<%= error_tag f, :title %>
</div>
<div class="form-group">
<%= textarea f, :body, placeholder: "Body", class: "form-control" %>
<%= error_tag f, :body %>
</div>
<%= submit "Create post", class: "btn btn-primary" %>
<% end %>
# web/templates/post/edit.html.eex<h1>Edit post</h1><%= form_for @changeset, user_post_path(@conn, :update, @current_user,
@post), fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>There are some errors</p>
</div>
<% end %>
<div class="form-group">
<%= text_input f, :title, placeholder: "Title", class: "form-control" %>
<%= error_tag f, :title %>
</div>
<div class="form-group">
<%= textarea f, :body, placeholder: "Body", class: "form-control" %>
<%= error_tag f, :body %>
</div>
<span>
<%= submit "Edit", class: "btn btn-primary" %>
&nbsp;
<%= link "Cancel", to: user_post_path(@conn, :show, @current_user,
@post) %>
</span>
<% end %>

It’s time to try out our posts CRUD code. Start the server, sign in and create a post. Feel free to try update/delete it. Next, go to a post show page and copy the link to this page. Now, sign out and try to proceed copied link. You’ll see the post. It’s not good, because we claimed: a guest can read nothing. We need to check if user is authenticated. How?

Guardian plugs and Phoenix router piplines will help us. Remember, we defined :login_required pipeline and used it in our ‘registered user zone’ scope? It’s time to implement it:

# web/router.expipeline :login_required do
plug Guardian.Plug.EnsureAuthenticated,
handler: SimpleAuth.GuardianErrorHandler
end

From guardian documentation:

Guardian.Plug.EnsureAuthenticated

Looks for a previously verified token. If one is found, continues, otherwise it will call the :unauthenticated function of your handler.

Ok. So we need to create SimpleAuth.GuardianErrorHandler module with unauthenticated/2 function. Let’s do it:

# web/auth/guardian_error_handler.exdefmodule SimpleAuth.GuardianErrorHandler do
import SimpleAuth.Router.Helpers
def unauthenticated(conn, _params) do
conn
|> Phoenix.Controller.put_flash(:error,
"You must be signed in to access that page.")
|> Phoenix.Controller.redirect(to: session_path(conn, :new))
end
end

The implementation of our SimpleAuth.GuardianErrorHandler is very simple: we’ve defined required ‘callback’ function and just declared what to do if our application receives a request from unauthenticated user. In our case we just display a flash message and redirect to sign in page.

Try to sign out and refresh the show-post page (copy-paste any post’s direct URI). You should see something like this:

What if we will try to edit other user’s post by direct link (we need to be signed in)?

It looks scary, but this error is expected, because we’re trying to fetch a post from current user posts list and can not find a post if user is not the owner of this post. Fortunately, we can handle it with ease, utilizing SimpleAuth.ErrorView:

# web/controllers/post_controller.ex# ...
def edit(conn, %{"id" => id}, current_user) do
post = current_user |> user_post_by_id(id)
if post do
changeset = Post.changeset(post)
render(conn, "edit.html", post: post, changeset: changeset)
else
conn
|> put_status(:not_found)
|> render(SimpleAuth.ErrorView, "404.html")
end
end
# ...

Refresh the page and you will see:

Great! We’ve implemented another part of our authorization system. And now have authentication and authorization for guests and registered users (posts authors). At the final part of this tutorial we will implement admin zone with ‘namespaced’ admin controllers and special admin routing scope.

Since we don’t want provide to a user the possibility to set up on his own will whether to be an admin or not, we have the need to have an admin in our system. I suggest you to use seeds.exs for this tutorial.

# priv/repo/seeds.exsalias SimpleAuth.Repo
alias SimpleAuth.User
admin_params = %{name: "Admin User",
email: "admin@test.com",
password: "supersecret",
is_admin: true}
unless Repo.get_by(User, email: admin_params[:email]) do
%User{}
|> User.registration_changeset(admin_params)
|> Repo.insert!
end

Run seeds and check if the admin has been created:

> mix run priv/repo/seeds.exs> iex -S mix
iex(1)> alias SimpleAuth.Repo
SimpleAuth.Repo
iex(2)> alias SimpleAuth.User
SimpleAuth.User
iex(4)> User |> Repo.get_by(email: "admin@test.com")
[debug] QUERY OK db=2.6ms
SELECT u0."id", u0."email", u0."name", u0."password_hash", u0."is_admin", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."email" = $1) ["admin@test.com"]
%SimpleAuth.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
email: "admin@test.com", id: 4, inserted_at: #Ecto.DateTime<2016-08-03 11:46:52>, is_admin: true, name: "Admin User", password: nil, password_hash: "$2b$12$sdgKUFBr5KKnaPRKPIutAuI2ksecovP5nNhEBXHhKuFDP//uWkpyW",
posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, updated_at: #Ecto.DateTime<2016-08-03 11:46:52>}

You can sign in with email and password you defined in seeds.exs and make sure that admin user seems to be like ordinary registered user in our application until we will implement special admin delete-any-post ability and admin zone.

Let’s start with an administrator’s ability to delete any post. Update delete action:

# web/controllers/post_controller.ex# ...
def delete(conn, %{"user_id" => user_id, "id" => id},
current_user) do
user = User |> Repo.get!(user_id)
post = user |> user_post_by_id(id) |> Repo.preload(:user) if current_user.id == post.user.id || current_user.is_admin do
Repo.delete!(post)
conn
|> put_flash(:info, "Post was deleted successfully")
|> redirect(to: user_post_path(conn, :index, user.id))
else
conn
|> put_flash(:info, "You can’t delete this post")
|> redirect(to: user_post_path(conn, :show, user.id, post.id))
end
end
# ...

We utilize user_id parameter from URI and check either current_user is the owner of the post or an admin. If so, we delete the post and redirect to its show page otherwise.

Also, do not forget to update post’s show page template:

# web/templates/post/show.html.eex<%= link "To list", to: user_post_path(@conn, :index, @post.user) %><h1><%= @post.title %></h1><p><%= @post.body %></p><%= if @current_user do %>
<%= if @current_user.id == @user.id do %>
<%= link "Edit", to: user_post_path(@conn, :edit,
@current_user, @post),
class: "btn btn-primary btn-xs" %>
<% end %>
<%= if @current_user.id == @user.id
|| @current_user.is_admin do %>
<%= link "Delete", to: user_post_path(@conn, :delete,
@post.user, @post),
method: :delete,
data: [confirm: "Really?"],
class: "btn btn-danger btn-xs"%>
<% end %>
<% end %>

Obviously, this is not the best template you’ve ever seen: we can use partials and view helpers here, for example, but it out of this tutorial scope.

Now, go experiment with deleting another user posts as admin (pay attention to signed in user (by email in the header)):

Now we’re ready to implement admin scope. Let’s recap router scopes before:

# web/router.ex# ...
# guest zone
scope "/", SimpleAuth do
pipe_through [:browser, :with_session]
get "/", PageController, :index resources "/sessions", SessionController, only: [:new, :create,
:delete]
resources "/users", UserController, only: [:new, :create] # registered user zone
scope "/" do
pipe_through [:login_required]
resources "/users", UserController, only: [:show] do
resources "/posts", PostController
end
# admin zone
scope "/admin", Admin, as: :admin do
pipe_through [:admin_required]
resources "/users", UserController, only: [:index, :show] do
resources "/posts", PostController, only: [:index, :show]
end
end
end
end
# ...

In /admin scope we should have following routes:

  • GET /admin/users/
  • GET /admin/users/:id
  • GET /admin/users/:user_id/posts
  • GET /admin/users/:user_id/posts/:id

Admin module as scope function argument defines ‘namespace’; as: :admin will add admin_ prefix to scoped routes. You can check it out by yourself with mix phoenix.routes task.

Firstly we’ll implement :admin_required pipeline:

# web/router.ex# ...
pipeline :admin_required do
plug SimpleAuth.CheckAdmin
end
# ...

Here is the plug’s code:

# web/auth/check_admin.exdefmodule SimpleAuth.CheckAdmin do
import Phoenix.Controller
import Plug.Conn
def init(opts), do: opts def call(conn, _opts) do
current_user = Guardian.Plug.current_resource(conn)
if current_user.is_admin do
conn
else
conn
|> put_status(:not_found)
|> render(SimpleAuth.ErrorView, "404.html")
|> halt
end
end
end

We simply get current user from Guardian’s current_resource and check for it is_admin property. If it is true then we merely return conn structure unmodified or render 404-error and halt the connection otherwise.

Let’s start with admin’s SimpleAuth.UserController and it’s index action:

# web/controllers/admin/user_controller.exdefmodule SimpleAuth.Admin.UserController do
use SimpleAuth.Web, :controller
alias SimpleAuth.User def index(conn, _params) do
users = Repo.all(User)
render(conn, "index.html", users: users)
end
end
# web/views/admin/user_view.exdefmodule SimpleAuth.Admin.UserView do
use SimpleAuth.Web, :view
end
# web/templates/admin/user/index.html.eex<h1>Users List</h1><ul>
<%= for user <- @users do %>
<li>
<%= link user.email,to: admin_user_path(@conn, :show, user) %>
</li>
<% end %>
</ul>

As you’ve noticed, we’ve created /admin directory in web/controllers, web/views and web/templates directories in respect of Phoenix naming convention. Implementation itself is very simple and don’t need to be explained.

Also, add a link to the main layout’s header:

# web/templates/layout/app.html.eex# ...
<ol class="breadcrumb text-right">
<%= if @current_user do %>
<li>
<%= @current_user.email %>
</li>
<li>
<%= link "My posts", to: user_post_path(@conn, :index,
@current_user) %>
</li>
<%= if @current_user.is_admin do %>
<li>
<%= link "Users", to: admin_user_path(@conn, :index) %>
</li>
<% end %>
<li>
<%= link "Sign out", to: session_path(@conn, :delete,
@current_user), method: "delete" %>
</li>
<% else %>
<li><%= link "Register", to: user_path(@conn, :new) %></li>
<li><%= link "Sign in", to: session_path(@conn, :new) %></li>
<% end %>
</ol>
# ...

And check users list out:

What if we’ll try to open that page by direct URI either as guest or non-admin registered user? If you’ll try it as guest a request will not pass :login_required pipeline and will be redirected to sign in page. If you’ll try as non-admin user — you’ll get ‘page not found’. And it’s all has been done by Phoenix plugs and pipelines, nice!

Here is show action implementation:

# web/controllers/admin/user_controller.ex# ...
def show(conn, %{"id" => id}) do
user = User |> Repo.get!(id)
render(conn, "show.html", user: user)
end
# ...
# web/templates/admin/user/show.html.eex<h1><%= @user.name %></h1><ul>
<li><%= @user.name %></li>
<li><%= @user.email %></li>
</ul>

And now, feel free to implement posts part of admin scope by yourself:

# web/router.ex# ...
scope "/admin", Admin, as: :admin do
pipe_through [:admin_required]
resources "/users", UserController, only: [:index, :show] do
resources "/posts", PostController, only: [:index, :show]
end
end
# ...

If there will be any issue you can always compare your solution with mine, posted on Github.

There can be an opinion that there was a lot of implementation code for simple authentication/authorization mechanism. But in closer look you can see that the significant part of codebase related to general Phoenix web application stuff. You can always reduce the amount of code with refatoring or third-party libraries (like Canary) usage.

When I started this tutorial I set the goal: show the power of Phoenix’s plugs and pipelines with meaningful (and useful) but simple example; implement the example step-by-step explaining interesting moments at the same time.

I hope I’ve reached this goal. Thank you for your time. :)

--

--