We can't find the internet
Something went wrong!
Ueberauth and guardian setup for a Phoenix rest API
- Created: 09/02/2018
- Last updated: 13/12/2024
Depending on what you are after, the official, built in generators might be a better way to go.
So Ivan asked me how to set up token based sessions and password authentication for an Elixir Phoenix Rest API. He then said ‘Have you considered writing a blog about this?’, then I did, and then I did…
My goal is to set up username / password authentication, and token based authorisation using JWT for rest API endpoints, with some routes restricted to users with set permissions.
When doing this for the first time with Phoenix, I didn’t find it entirely obvious which combination of packages are needed, or how to get started with them. So this is an attempt at a basic getting started guide, which you can then build on to make a production worthy setup which fits your own requirements.
I have posted the resulting example application on github, complete with docker file so that you can see it run:
https://gitlab.com/nathamanath/phoenix_auth_example
Baseline…
For anyone who would like to play along at home, set up a new phoenix app; I’m
on version 1.3
.
Getting started
First, get the required packages… add the following to your mix.exs
and i’ll
explain why we need each lib. Also :ueberauth
, and :ueberauth_identity
need
adding to extra_applications
.
{:ueberauth, "~> 0.5.0"},
{:ueberauth_identity, "~> 0.2.3"},
{:guardian, "~> 1.0"},
{:comeonin, "~> 4.1"},
{:bcrypt_elixir, "~> 1.0"}
ueberauth is ‘An Elixir Authentication System for Plug-based Web Applications’. It is the main framework we will use for authenticating users.
ueberauth_identity is ‘A simple username/password strategy for Uberauth’. Ueberauth dosent assume a particular authorisation strategy, instead, strategy libs are made available here: https://github.com/ueberauth, or you can make your own.
We want username / password auth, so ueberauth_identity
is the one for us.
guardian is ‘An authentication library for use with Elixir applications.’ We are going to use guardian to manage user session tokens. It will encode and validate JWT for us, aswell as managing individual users permissions.
We will use comeonin to hash, and check user passwords.
Comeonin depends on a password hashing lib. You can choose Argon2, Bcrypt, or Pbkdf2. We will use bcrypt_elixir
This package is the bcrypt password hashing algorithm for Elixir. Do take note that this lib requires >1 CPU core to function. If you have only one core, on say a small VPS, your release will crash without giving a useful error message!!
For a single core host, use Pbkdf2 instead of Bcrypt. See here for more on this: https://github.com/riverrun/comeonin/wiki/Deployment
Give it a mix deps.get
and we can get to the authorising…
The user resource
So we will need some users to authorise. Im going to generate an uber simple user resource.
mix phx.gen.json \
Accounts \
User \
users \
username:string:unique \
hashed_password:string \
permissions:map
Add it to your router in the api scope like so:
scope "/api", AuthWeb do
pipe_through :api
resources "/users", UserController, except: [:new, :edit]
end
and mix ecto.migrate
.
We need to make a few modifications to lib/auth/accounts/user.ex
for this to
be useful to us (see comments)…
defmodule Auth.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias Auth.Accounts.User
schema "users" do
field :hashed_password, :string
field :permissions, :map
field :username, :string
# Add a virtual attribute to hold plain text passwords.
field :password, :string, virtual: true
timestamps()
end
@doc false
def changeset(%User{} = user, attrs) do
user
# Cast and require a password for each user
|> cast(attrs, [:username, :password, :permissions])
|> validate_required([:username, :password, :permissions])
|> unique_constraint(:username)
# Hash passwords before saving them to the database.
|> put_hashed_password()
end
defp put_hashed_password(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :hashed_password, Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
end
And at this point, I am going to seed a few users with a range of permissions now so that we can try out our code as we progress…
https://gitlab.com/nathamanath/phoenix_auth_example/blob/master/priv/repo/seeds.exs
And after seeding the database, Auth.Accounts.list_users
will show us our
seeded users and that our changes to Auth.Accounts.User
have worked as
intended.
[%Auth.Accounts.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
hashed_password: "$2b$12$ZPxcbPz9c.JyUKdxefgIB.L0SxcPTmu1qzB/Ki8yS66lGL...",
id: 1,
inserted_at: ~N[2018-02-09 08:38:52.705037],
password: nil,
updated_at: ~N[2018-02-09 08:38:52.705046],
username: "reader"
}, ... ]
Accounts context
Next job is to be able to confirm username and password combinations…
To the accounts context!
Here we will use Comeonin.Bcrypt.checkpw/2
to compare a password with a
password hash in our database, and Comeonin.Bcrypt.dummy_checkpw/2
when an
incorrect username is given. This is to help prevent a possible
timing attack in which an
attacker could infer whether email addresses are present in the database or not.
def get_user_by_username_and_password(nil, password), do: {:error, :invalid}
def get_user_by_username_and_password(username, nil), do: {:error, :invalid}
def get_user_by_username_and_password(username, password) do
with %User{} = user <- Repo.get_by(User, username: String.downcase(username)),
true <- Comeonin.Bcrypt.checkpw(password, user.hashed_password) do
{:ok, user}
else
_ ->
# Help to mitigate timing attacks
Comeonin.Bcrypt.dummy_checkpw
{:error, :unauthorised}
end
end
If you chose a different hashing lib to use with comeonin, then switch out the
Comeonin.Bcrypt
module for whichever other one you selected. You can call
checkpw/2
and dummy_checkpw/0
on each of the hashing modules mentioned
above.
Now we can check that Accounts.get_user_by_username_and_password/2
works as
expected; All going well, in iex
you should get stuff like this:
Auth.Accounts.get_user_by_username_and_password "reader", "qweqweqwe"
{:ok,
# => %Auth.Accounts.User{...}
Auth.Accounts.get_user_by_username_and_password "frank", "qweqweqwe"
# => {:error, :unauthorised}
Auth.Accounts.get_user_by_username_and_password "reader", "qweqweqwe"
# => {:error, :unauthorised}
Authentication controller
Now that this works, I want to see the same thing over HTTP. To achieve this we need to:
- Configure ueberauth (and ueberauth_identity).
- Configure guardian.
- Write a guardian token module.
- Implement the actual authentication controller using the above dependencies.
- Update our router.
For this example, in config.exs
, we will configure ueberauth and guardian like
so:
config :ueberauth, Ueberauth,
base_path: "/api/auth",
providers: [
identity: {Ueberauth.Strategy.Identity, [
callback_methods: ["POST"],
nickname_field: :username,
param_nesting: "user",
uid_field: :username
]}
]
config :auth, Auth.Guardian,
issuer: "Auth",
secret_key: "use mix phx.gen.secret yo",
# We will get round to using these permissions at the end
permissions: %{
default: [:read_users, :write_users]
}
We set up ueberauth, with the ueberauth identity strategy as an authorisation provider. For this you have to set:
- The request method which our controller will accept authorisation attempts on.
- The path which we will map to the identity callback controller action via our router.
-
nickname_field
is used to map our user resource to a jason web token. -
param_nesting
anduid_field
tells ueberauth that we will be posting up a user object containing theusername
andpassword
attributes. And that we will use theusername
field to identify our users.
In order to issue tokens we need to write a module using the guardian behaviour:
defmodule Auth.Guardian do
use Guardian, otp_app: :auth
def subject_for_token(%{id: id}, _claims) do
{:ok, to_string(id)}
end
def subject_for_token(_, _) do
{:error, :no_resource_id}
end
def resource_from_claims(%{"sub" => sub}) do
{:ok, Auth.Accounts.get_user!(sub)}
end
def resource_from_claims(_claims) do
{:error, :no_claims_sub}
end
end
This module (along with guardian config) declares how we will encode and decode our user resource as a JWT.
And with that, we have all we need to put together an authentication controller:
defmodule AuthWeb.AuthenticationController do
use AuthWeb, :controller
alias Auth.Accounts
plug Ueberauth
def identity_callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
username = auth.uid
password = auth.credentials.other.password
handle_user_conn(Accounts.get_user_by_username_and_password(username, password), conn)
end
defp handle_user_conn(user, conn) do
case user do
{:ok, user} ->
{:ok, jwt, _full_claims} =
Auth.Guardian.encode_and_sign(user, %{})
conn
|> put_resp_header("authorization", "Bearer #{jwt}")
|> json(%{data: %{token: jwt}})
# Handle our own error to keep it generic
{:error, _reason} ->
conn
|> put_status(401)
|> json(%{message: "user not found"})
end
end
end
Next, map a route to the identity_callback
action like so:
scope "/api", AuthWeb do
pipe_through :api
scope "/auth" do
post "/identity/callback", AuthenticationController, :identity_callback
end
resources "/users", UserController, except: [:new, :edit]
end
With that, we should be able to authenticate over HTTP
curl \
-XPOST \
-v \
localhost:4000/api/auth/identity/callback \
-H 'content-type: application/json' \
-d '{"user": {"username": "reader", "password": "qweqweqwe"}}'
# => {"data": {"token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBdXRoIiwiZ..."}}
You should now see a 200 status and a lovely JWT when using correct credentials, and a 401 accompanied by an annoying message when you try without.
Authentication pipeline
This is going well. Now that we can issue session tokens to authorised users, we can restrict access to some api endpoints based on having aquired a valid JWT.
This will be carried out using a series of plugs provided by Guardian.
We write our own plug, grouping them together so that we can control how errors are handled:
defmodule AuthWeb.Plug.AuthAccessPipeline do
use Guardian.Plug.Pipeline, otp_app: :auth
plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
plug Guardian.Plug.EnsureAuthenticated
plug Guardian.Plug.LoadResource, ensure: true
end
We want to ensure that our authentication pipeline gives nice generic errors to anyone who might be password guessing, for this we write another plug:
defmodule AuthWeb.Plug.AuthErrorHandler do
import Plug.Conn
import Phoenix.Controller, only: [json: 2]
def auth_error(conn, {type, _reason}, _opts) do
conn
|> put_status(401)
|> json(%{message: to_string(type)})
|> halt()
end
end
And the configure Auth.AuthAccessPipeline
to use it:
# Configure the authentication plug pipeline
config :auth, AuthWeb.Plug.AuthAccessPipeline,
module: Auth.Guardian,
error_handler: AuthWeb.Plug.AuthErrorHandler
The last step in this section is to add a pipeline to the router,
pipeline :authenticated do
plug AuthWeb.Plug.AuthAccessPipeline
end
and to use it to restrict access to our user resource:
scope "/api", AuthWeb do
pipe_through :api
# SNIP...
pipe_through :authenticated
resources "/users", UserController, except: [:new, :edit]
end
With all of that in place, we can try hitting up /api/users
both with, and
without a valid session token:
curl localhost:4000/api/users -H 'content-type: application/json'
# => {"message":"unauthenticated"}
# get an auth token
curl \
-XPOST \
localhost:4000/api/auth/identity/callback \
-H 'content-type: application/json' \
-d '{"user": {"username": "reader", "password": "qweqweqwe"}}'
# => {"data": {"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiO..."}}
# use the auth token in the authorisation header like so:
curl localhost:4000/api/users \
-H 'content-type: application/json' \
-H 'authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiO...'
# => {"data":[{"username":"reader","id":1,"hashed_password":"$2b$12....]}
By the way, now that our authentication pipeline is in place,
Guardian.Plug.current_resource(conn)
will get us the current logged in user.
User permissions
Lastly lets have a quick look at managing user permissions. I want different authenticated users to have access to differing api endpoints.
Auth.Accoutns.User
already has a permissions attribute holding a list of
permission names, and so lets put those to work.
Guardian provides the functionality to encode and check user permissions for us
in the Guardian.Permissions.Bitwise
behaviour and a corresponding
Guardian.Permissions.Bitwise
plug.
First we need to update Auth.Guardian to encode a users permissions into their tokens:
defmodule Auth.Guardian do
@moduledoc """
Integration with Guardian
"""
use Guardian, otp_app: :auth
use Guardian.Permissions.Bitwise
# SNIP...
def build_claims(claims, _resource, opts) do
claims =
claims
|> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
{:ok, claims}
end
end
And pass in a user’s permissions when calling Auth.Guardian.encode_and_sign/3
from Auth.AuthenticationController
Auth.Guardian.encode_and_sign(user, %{}, permissions: user.permissions)
Now, in Auth.UserController
we can check these permissions per controller
action with the Guardian.Permissions.Bitwise
plug:
plug Guardian.Permissions.Bitwise, ensure: %{default: [:read_users]}
plug Guardian.Permissions.Bitwise, [ensure: %{default: [:write_users]}] when action in [:create, :update, :delete]
Now try authenticating as each of our seeded users, and hitting up each user endpoint. You will see that whilst all users can reach the authentication controller, but, as we ordained it to be:
-
Only
writer
can get to:create
,:update
,:delete
-
reader
can only access:show
and:index
-
And
rubbish
can’t access this resource at all
Next steps
We have the start of a flexible user authentication and authorisation setup for our Phoenix Elixir restful API, but there is more attention to detail required to make this production worthy.
A bunch of things are missing, which you would probably like in your own implementation before using it to secure your application.
For example:
- Enforce strong password choices
- Implement a password reset feature
- Write tests, so that you can be sure that your code behaves as intended
- Rate limit authentication attempts
- Role management
- Implement other authentication strategies, such as oauth2 via a third party provider
- TTL on your tokens, and a means of refreshing them
And of course, TLS is essential when handling user credentials.
There is much more detail in the docs and readmes of each of the packages used (links below). It’s well worth having a look to see the options available to you here. Also, if one or more of these packages is not to your liking, there are alternatives to each available on hex.