View Source Getting Started with Guardian
Getting started with Guardian is easy. This tutorial will cover
- Setting up the basics of Guardian
- HTTP integration
- Login/Logout
This tutorial was based on this article by Tyler Pachal.
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.
We'll also use the default token type of JWT. 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
specify-your-dependencies
Specify your dependencies
You'll need to update the dependencies to whatever is latest.
## mix.exs
defp deps do
[
{:guardian, "~> 2.0"},
{:argon2_elixir, "~> 2.0"},
]
end
create-a-user-manager
Create a user manager
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 (but see the note after the command).
$ mix phx.gen.context UserManager User users username:string password:string
Note: if you already have a User model, you need to implement some of the functions generated by phx.gen.context, which are:
def list_users do
Repo.all(User)
end
def get_user!(id), do: Repo.get!(User, id)
#The tutorial calls this one:
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
def delete_user(%User{} = user) do
Repo.delete(user)
end
#The tutorial calls this one:
def change_user(%User{} = user) do
User.changeset(user, %{})
end
create-implementation-module
Create implementation module
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
user = UserManager.get_user!(id)
{:ok, user}
rescue
Ecto.NoResultsError -> {:error, :resource_not_found}
end
end
subject_for_token
and resource_from_claims
are inverses of one another. subject_for_token
is used to encode the User into the token, and resource_from_claims
is used to rehydrate the User from the claims.
There are many other callbacks that you can use, but we're going basic.
setup-guardian-config
Setup Guardian config
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.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.
password-hashing
Password hashing
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 :comeonin
and :argon2_elixir
to our mix deps at the start. We're going to use them in two places.
- When setting the password for the user
- When verifying the login credentials
## lib/auth_me/user_manager/user.ex
alias Argon2
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password])
|> validate_required([:username, :password])
|> 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
import Ecto.Query, only: [from: 2]
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 {:ok, user} == Argon2.check_pass(user, "some password", hash_key: :password)
assert user.username == "some username"
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 {:ok, user} == Argon2.check_pass(user, "some updated password", hash_key: :password)
assert user.username == "some updated username"
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.
pipelines
Pipelines
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 User 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 User 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
controller
Controller
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() #This module's full name is Auth.UserManager.Guardian.Plug,
|> redirect(to: "/login") #and the arguments specified in the Guardian.Plug.sign_out()
end #docs are not applicable here
defp login_reply({:ok, user}, conn) do
conn
|> put_flash(:info, "Welcome back!")
|> Guardian.Plug.sign_in(user) #This module's full name is Auth.UserManager.Guardian.Plug,
|> redirect(to: "/protected") #and the arguments specified in the Guardian.Plug.sign_in()
end #docs are not applicable here.
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_ex_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 protected(conn, _) do
user = Guardian.Plug.current_resource(conn)
render(conn, "protected.html", current_user: user)
end
def index(conn, _params) do
render(conn, "index.html")
end
end
We use the Guardian.Plug.current_resource(conn)
function here to fetch the User. 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>
routes
Routes
Okay. 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.
# 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 User but that is not required for ensure auth.
try-it-out
Try it out
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:
iex(1)> AuthMe.UserManager.create_user(%{username: "me", password: "secret"})
Now exit iex and start up your server:
$ mix phx.server
Enter localhost:4000/protected
in your browser's address bar, and you should see "unauthenticated".
Now, enter localhost:4000/login
in your browser's address bar, and login with your me
username and secret
password.
You should automatically be redirected to the protected page, which you can now see!
To logout, enter localhost:4000/logout
in your browser's address bar, and you will be redirected to the login page.
Instead of logging in, enter localhost:4000/protected
in your browser's address bar, and you will see "unauthenticated" again!