Getting started with Guardian is easy. This tutorial will cover
- Setting up the basics of Guardian
- HTTP integration
- Login/Logout
We'll use Phoenix for this tutorial since most folks will be using it. There is no requirement to use Phoenix with Guardian but it makes this tutorial easier.
This tutorial was based on this article by Tyler Pachal.
We'll also use the default token type of JWT. Again with this you don't have to use JWT for your token backend. See the token documentation for more information.
Authentication consists of a challenge phase (prove who you are) and then followed by a verification phase (has this actor proven who they are?). Guardian looks after the second part for you. It's up to your application to implement the challenge phase after which Guardian will do the rest. In this tutorial we'll use comeonin with argon2 for the challenge phase.
Let's generate an application.
$ mix phx.new auth_me
You'll need to update the dependencies to whatever is latest.
## mix.exs
...
defp deps do
[
...
{:guardian, "~> 1.2"},
{:argon2_elixir, "~> 2.0"},
...
]
end
...
We'll need something to authenticate. How Users are created and what they can do is outside the scope of this tutorial. If you already have a user model you can skip this part.
$ mix phx.gen.context UserManager User users username:string:unique email:string:unique password:string
Guardian needs an implementation. This implementation module encapsulates:
- Token type
- Configuration
- Encoding/Decoding
- Callbacks
For more information please reference the implementation module docs.
You can have as many implementation modules as you need to depending on your application. For this one though we only have a simple user system so we'll only need one.
## lib/auth_me/user_manager/guardian.ex
defmodule AuthMe.UserManager.Guardian do
use Guardian, otp_app: :auth_me
alias AuthMe.UserManager
def subject_for_token(user, _claims) do
{:ok, to_string(user.id)}
end
def resource_from_claims(%{"sub" => id}) do
case UserManager.get_user!(id) do
nil -> {:error, :resource_not_found}
user -> {:ok, user}
end
end
end
subject_for_token
and resource_from_claims
are inverses of one another. subject_for_token
is used to encode the resource into the token, and resource_from_claims
is used to rehydrate the resource from the claims.
There are many other callbacks that you can use, but we're going basic.
To use the JWT token type, we'll need a secret. We'll use the HS512
algorithm which is a simple hashing algorithm. The most basic configuration is very straight forward.
The secret can be any string but it's recommended that you use the generator provided with Guardian for this.
$ mix guardian.gen.secret
Copy the output from the previous command and add it to your configuration.
## config/config.exs
...
config :auth_me, AuthMe.UserManager.Guardian,
issuer: "auth_me",
secret_key: "" # put the result of the mix command above here
...
You should change the secret key for each environment and manage them with your secret management strategy.
This too is not strictly required for Guardian. If you already have a way for you to verify user/password for your user model you can skip this part.
We'll implement a simple version of password hashing for this tutorial. This is up to your application and is only shown here for example purposes.
We added :argon2_elixir
to our mix deps at the start. We're going to use it in two places.
- When setting the password for the user
- When verifying the login credentials
## lib/auth_me/user_manager/user.ex
...
alias Argon2
alias AuthMe.UserManager.User
...
...
def changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:username, :email, :password])
|> validate_required([:username, :email, :password])
|> unique_constraint(:username)
|> unique_constraint(:email)
|> put_password_hash()
end
defp put_password_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
change(changeset, password: Argon2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
...
Now we need a way to verify the username/password credentials.
## lib/auth_me/user_manager.ex
...
alias Argon2
...
...
def authenticate_user(username, plain_text_password) do
query = from u in User, where: u.username == ^username
case Repo.one(query) do
nil ->
Argon2.no_user_verify()
{:error, :invalid_credentials}
user ->
if Argon2.verify_pass(plain_text_password, user.password) do
{:ok, user}
else
{:error, :invalid_credentials}
end
end
end
...
At this point, the autogenerated tests when we first created the UserManager and User (if you followed this guide to the letter) would fail when you run mix test
. This is due to the fact that the autogenerated tests don't know about passwords - all it knows is your Users table has two string columns that it needs to check.
Fixing the broken tests is simple: we want to compare the encrypted passwords instead of plain text ones.
## test/auth_me/user_manager_test.exs
...
test "create_user/1 with valid data creates a user" do
assert {:ok, %User{} = user} = UserManager.create_user(@valid_attrs)
assert user.email == "some email"
assert user.username == "some username"
assert {:ok, user} == Argon2.check_pass(user, "some password", hash_key: :password)
end
...
test "update_user/2 with valid data updates the user" do
user = user_fixture()
assert {:ok, %User{} = user} = UserManager.update_user(user, @update_attrs)
assert user.email == "some updated email"
assert user.username == "some updated username"
assert {:ok, user} == Argon2.check_pass(user, "some updated password", hash_key: :password)
end
...
That's it for Guardian setup. We had some User setup in there too that was unrelated but if you don't have a user model it will help you get started.
The next step is getting it into your application via HTTP.
For HTTP Guardian makes use of the Plug architecture and uses it to construct pipelines. The pipeline provides downstream plugs with the implementation module and the error handler that the Guardian plugs require to do their job.
Please read the pipeline guide for more information.
We want our pipeline to look after session and header authentication (where to look for the token), load the resource but not enforce it. By not enforcing it we can have a "logged in" or "maybe logged in". We can use the Guardian.Plug.EnsureAuthenticated plug for those cases where we must have a logged in resource by using Phoenix pipelines in the router.
## lib/auth_me/user_manager/pipeline.ex
defmodule AuthMe.UserManager.Pipeline do
use Guardian.Plug.Pipeline,
otp_app: :auth_me,
error_handler: AuthMe.UserManager.ErrorHandler,
module: AuthMe.UserManager.Guardian
# If there is a session token, restrict it to an access token and validate it
plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
# If there is an authorization header, restrict it to an access token and 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'll also need the error handler referenced in our pipeline to handle the case where there was a failure to authenticate.
## lib/auth_me/user_manager/error_handler.ex
defmodule AuthMe.UserManager.ErrorHandler do
import Plug.Conn
@behaviour Guardian.Plug.ErrorHandler
@impl Guardian.Plug.ErrorHandler
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
This pipeline is now ready for us to use. Now we need some way to login/logout the user and some resource to protect. For this we'll create a sessions controller, and use the PageController for our protected resource
## lib/auth_me_web/controllers/session_controller.ex
defmodule AuthMeWeb.SessionController do
use AuthMeWeb, :controller
alias AuthMe.{UserManager, UserManager.User, UserManager.Guardian}
def new(conn, _) do
changeset = UserManager.change_user(%User{})
maybe_user = Guardian.Plug.current_resource(conn)
if maybe_user do
redirect(conn, to: "/protected")
else
render(conn, "new.html", changeset: changeset, action: Routes.session_path(conn, :login))
end
end
def login(conn, %{"user" => %{"username" => username, "password" => password}}) do
UserManager.authenticate_user(username, password)
|> login_reply(conn)
end
def logout(conn, _) do
conn
|> Guardian.Plug.sign_out()
|> redirect(to: "/login")
end
defp login_reply({:ok, user}, conn) do
conn
|> put_flash(:info, "Welcome back!")
|> Guardian.Plug.sign_in(user)
|> redirect(to: "/protected")
end
defp login_reply({:error, reason}, conn) do
conn
|> put_flash(:error, to_string(reason))
|> new(%{})
end
end
Create a session view
## lib/auth_me_web/views/session_view.ex
defmodule AuthMeWeb.SessionView do
use AuthMeWeb, :view
end
And for the login template and secret template:
## lib/auth_me_web/templates/session/new.html.eex
<h2>Login Page</h2>
<%= 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 %>
Lets make the protected resource implementation.
## lib/auth_me_web/controllers/page_controller.ex
defmodule AuthMeWeb.PageController do
use AuthMeWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
def protected(conn, _) do
user = Guardian.Plug.current_resource(conn)
render(conn, "protected.html", current_user: user)
end
end
We use the Guardian.Plug.current_resource(conn)
function here to fetch the resource. You must load this first using the Guardian.Plug.LoadResource
plug which we included in our auth pipeline earlier.
## lib/auth_me_web/templates/page/protected.html.eex
<h2>Protected Page</h2>
<p>You can only see this page if you are logged in</p>
<p>You're logged in as <%= @current_user.username %></p>
Ok. So the controller and views are not strictly part of Guardian but we need some way to interact with it. From here the only thing left for us to do is to wire up our router.
## lib/auth_me_web/router.ex
...
# Our pipeline implements "maybe" authenticated. We'll use the `:ensure_auth` below for when we need to make sure someone is logged in.
pipeline :auth do
plug AuthMe.UserManager.Pipeline
end
# We use ensure_auth to fail if there is no one logged in
pipeline :ensure_auth do
plug Guardian.Plug.EnsureAuthenticated
end
# Maybe logged in routes
scope "/", AuthMeWeb do
pipe_through [:browser, :auth]
get "/", PageController, :index
get "/login", SessionController, :new
post "/login", SessionController, :login
get "/logout", SessionController, :logout
end
# Definitely logged in scope
scope "/", AuthMeWeb do
pipe_through [:browser, :auth, :ensure_auth]
get "/protected", PageController, :protected
end
...
There's a little bit happening here.
- We created a Phoenix pipeline that just delegates to our Guardian pipeline to login someone if we find a token in the session or header. This does not restrict access.
- We restrict access using both our
:auth
and:ensure_auth
phoenix pipelines and use that to protect our protected route.
Note that you must use the :auth
pipeline before the :ensure_auth
one to make sure that we have fetched and verified the token. We're also loading the resource but that is not required for ensure auth.
Migrate your users table.
mix ecto.migrate
Since we didn't implement a form for creating a user we'll need to do that from the command line. Open up iex
iex -S mix
Create the user:
AuthMe.UserManager.create_user(%{username: "me", email: "[email protected]", password: "secret"})
Now exit and start up your server:
mix phx.server
Open up localhost:4000/login
and you should be able to login with your me
secret
credentials!