Session Authentication Example For Phoenix 1.3 Using Guardian 1.0-beta

Tyler Pachal
Oct 23, 2017 · 6 min read

If you are in a hurry you can just copy all of the code and paste it into the file that is indicated. There are also a few mix commands to run.

UPDATE: This article was written about the beta version and things have changed slightly. This article is still mostly accurate, but there is a more up-to-date version of this tutorial in the Guardian Repo.


Recently I have been programming in Elixir for work, and wanted to play around with Phoenix for a personal project. My project needed to have different “zones” of authentication:

  • logged in
  • maybe logged in

I created a small example project to test authentication (github link), which we will re-produce in this post.

Image for post
Demo of the example app

0) Create a Phoenix Application

For this exercise I created a new Phoenix application called auth_ex (authentication_example) by running:

## On the command linemix phx.new auth_ex

If you already have an application you don’t need this step, just note that file paths will be different, and to replace occurences of :auth_ex with the atom for your project.

1) Specify Dependencies

We are going to be using Guardian (currently version 1.0-beta) for authentication, and Comeonin (with bcrypt) for our password hashing. Add the dependencies to your mix.exs file:

## mix.exsdefp deps do
[
{:guardian, "~> 1.0-beta"},
{:comeonin, "~> 4.0"},
{:bcrypt_elixir, "~> 0.12"}
]
end

2) Create a User Model

Guardian uses Json Web Tokens (JWT) to keep track of sessions by storing claims in an encoded JSON object. For our project, we will be doing a simple claim based on a user model. Generate your user model like this, unless you already have one in your project:

## On the command linemix phx.gen.context Auth User users username:string password:string

3) Implement Guardian Callbacks

Now that we have a user model, we need to implement a few callback functions that Guardian relies on for using our model:

## lib/auth_ex/auth/guardian.exdefmodule AuthEx.Auth.Guardian do
use Guardian, otp_app: :auth_ex
alias AuthEx.Auth def subject_for_token(user, _claims) do
{:ok, to_string(user.id)}
end
def resource_from_claims(claims) do
user = claims["sub"]
|> Auth.get_user!
{:ok, user}
# If something goes wrong here return {:error, reason}
end
end

The subject_for_token needs to return something that can identify our user, so we will return the id field. The resource_from_claims is just the opposite, where we extract an id from the claims of JWT, then return the matching user.

4) Password Hashing

We added :comeonin and :bcrypt_elixir to our dependencies for having the passwords we are storing. This happens in two places.

First is when a new user is created, the incoming request gives us a plain-text password, and we will need to hash it before storing it in our database. Alter the user model’s changeset like so:

## lib/auth_ex/auth/user.exalias Comeonin.Bcryptdef changeset(%User{} = user, attrs) do    
user
|> cast(attrs, [:username, :password])
|> validate_required([:username, :password])
|> put_pass_hash()
end
defp put_pass_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
change(changeset, password: Bcrypt.hashpwsalt(password))
end
defp put_pass_hash(changeset), do: changeset

Now we will also need a way to check the username and password we receive on a login request are valid. For that we will add a new function in the auth context, where will look for the user based on the username, and then check that the hash of the password matches what was in the database:

## lib/auth_ex/auth/auth.exalias Comeonin.Bcryptdef authenticate_user(username, plain_text_password) do
query = from u in User, where: u.username == ^username
Repo.one(query)
|> check_password(plain_text_password)
end
defp check_password(nil, _), do: {:error, "Incorrect username or password"}defp check_password(user, plain_text_password) do
case Bcrypt.checkpw(plain_text_password, user.password) do
true -> {:ok, user}
false -> {:error, "Incorrect username or password"}
end
end

5) Config

Next we need to add a bit of configuration. First generate yourself a secret, which will be used by Guardian to secure the JWTs that your application generates:

## On the command linemix guardian.gen.secret

Then add this to your config.exs file:

## config.exsconfig :auth_ex, AuthEx.Auth.Guardian,
issuer: "auth_ex", # Name of your app/company/product
secret_key: "" # Replace this with the output of the mix command

Note that in a production environment the secret_key is a secret and should not be hard-coded into your configuration file or checked into Github; you should replace it with an environment variable, but that is outside the scope of this post.

6) Pipelines

Now its time to create the pipleines for the different “zones” of authentication in the application (maybe-logged-in and definitely-logged-in). We will create the “base” pipeline first, which is our “maybe logged in” pipeline. The other pipeline will leverage this one, and be defiend in our router. This pipeline checks for a resource (a user) but does not reject the request if one is not found:

## lib/auth_ex/auth/pipeline.exdefmodule AuthEx.Auth.Pipeline do
use Guardian.Plug.Pipeline,
otp_app: :auth_ex,
error_handler: AuthEx.Auth.ErrorHandler,
module: AuthEx.Auth.Guardian
# If there is a session token, validate it
plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
# If there is an authorization header, validate it
plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
# Load the user if either of the verifications worked
plug Guardian.Plug.LoadResource, allow_blank: true
end

We will also have to create an error handler, which in our case will just return some text with the error:

## lib/auth_ex/auth/error_handler.exdefmodule AuthEx.Auth.ErrorHandler do
import Plug.Conn
def auth_error(conn, {type, _reason}, _opts) do
body = to_string(type)
conn
|> put_resp_content_type("text/plain")
|> send_resp(401, body)
end
end

7) Controller

Its time to setup our controller so that we can wire together the functionality we need to test our new pipeline. For my example I am just using the exisintg PageController with some login/logout stuff added on:

## lib/auth_ex_web/controllers/page_controller.exdefmodule AuthExWeb.PageController do
use AuthExWeb, :controller
alias AuthEx.Auth
alias AuthEx.Auth.User
alias AuthEx.Auth.Guardian
def index(conn, _params) do
changeset = Auth.change_user(%User{})
maybe_user = Guardian.Plug.current_resource(conn)
message = if maybe_user != nil do
"Someone is logged in"
else
"No one is logged in"
end
conn
|> put_flash(:info, message)
|> render("index.html", changeset: changeset, action: page_path(conn, :login), maybe_user: maybe_user)
end
def login(conn, %{"user" => %{"username" => username, "password" => password}}) do
Auth.authenticate_user(username, password)
|> login_reply(conn)
end
defp login_reply({:error, error}, conn) do
conn
|> put_flash(:error, error)
|> redirect(to: "/")
end
defp login_reply({:ok, user}, conn) do
conn
|> put_flash(:success, "Welcome back!")
|> Guardian.Plug.sign_in(user)
|> redirect(to: "/")
end
def logout(conn, _) do
conn
|> Guardian.Plug.sign_out()
|> redirect(to: page_path(conn, :login))
end
def secret(conn, _params) do
render(conn, "secret.html")
end
end

We will also need to update the index.html.eex to provide a place to log in an logout. This page will be visible regardless of if someone is logged in or not, but it will use the maybe_user value from our controller to present different things for each case:

## lib/auth_ex_web/templates/page/index.html.eex<h2>Login Page</h2><%= if @maybe_user == nil do %>
<%= form_for @changeset, @action, fn f -> %>

<div class="form-group">
<%= label f, :username, class: "control-label" %>
<%= text_input f, :username, class: "form-control" %>
<%= error_tag f, :username %>
</div>
<div class="form-group">
<%= label f, :password, class: "control-label" %>
<%= password_input f, :password, class: "form-control" %>
<%= error_tag f, :password %>
</div>
<div class="form-group">
<%= submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
<% else %>
<h1>Hello <%= @maybe_user.username%>!</h1>
<span><%= link "Logout", to: page_path(@conn, :logout), method: :post%></span>
<% end %>

And define the secret.html.eex file, which will only be visible to users that are logged in:

## lib/auth_ex_web/templates/page/secret.html.eex<h2>Secret Page</h2>
<p>You can only see this page if you are logged in</p>

8) Router

Now that everything is ready we can tie it all together by adding this in the router.ex file:

## lib/auth_ex_web/router.expipeline :auth do
plug AuthEx.Auth.Pipeline
end
pipeline :ensure_auth do
plug Guardian.Plug.EnsureAuthenticated
end
# Maybe logged in scope
scope "/", AuthExWeb do
pipe_through [:browser, :auth]
get "/", PageController, :index
post "/", PageController, :login
post "/logout", PageController, :logout
end
# Definitely logged in scope
scope "/", AuthExWeb do
pipe_through [:browser, :auth, :ensure_auth]
get "/secret", PageController, :secret
end

A couple things are happening here. We are defining two new pipes, :auth and :ensure_auth. The first one just calls the pipeline we created in step 6, and the second one is just composed of a single Guardian Plug that makes sure that somone is logged in.

Using the pipelines works as you would expect, just note that for the definitely-logged-in scope we construct the pipe_through using :auth and then :ensure_auth together (along with the default browser pipeline).

Testing

Since I didn’t create any of the forms for my user model I will just create one my opening up an iex session and creating a user via Repo. Enter into an iex session:

## On the command lineiex -S mix

And then create a user:

## On the command line (in a iex session)AuthEx.Auth.create_user(%{username: "tyler", password: "elixir"})

Now we are ready to test. As usual startup your phoenix app by running:

## On the command linemix phx.server

And open up localhost:4000 in your browser, where you will be able to login and logout of our application. Also test localhost:4000/secret in each state to observe that it is protected by the :ensure_auth pipeline from our router.

Github Repo

A completed working example of this code can be found here in a Github repository.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store