Most applications need some sort of authentication and authorization, and REST API's are no different. If you are familiar with web development but have never worked on one that does not have a front end (like me), then the authentication functionality might stump you at first.
What is Guardian?
Guardian is a token based authentication library for use with Elixir applications.
- More can be learned by reading its documentation, which I highly recommend.
- Keep in mind that the "tokens" that Guardians refers to are JSON Web Tokens.
Installation and Configuration
Add Guardian 1.0 as a dependency in mix.exs
.
{:guardian, "~> 1.0"}
And install
$ mix deps.get
Guardian requires that you add an implementation module, I added mine to an auth
folder in my web directory.
# lib/my_app_web/auth/guardian.ex
defmodule MyAppWeb.Guardian do
use Guardian, otp_app: :my_app
def subject_for_token(resource, _claims) do
# You can use any value for the subject of your token but
# it should be useful in retrieving the resource later, see
# how it being used on `resource_from_claims/1` function.
# A unique `id` is a good subject, a non-unique email address
# is a poor subject.
sub = to_string(resource.id)
{:ok, sub}
end
def resource_from_claims(claims) do
# Here we'll look up our resource from the claims, the subject can be
# found in the `"sub"` key. In `above subject_for_token/2` we returned
# the resource id so here we'll rely on that to look it up.
id = claims["sub"]
resource = MyApp.get_resource_by_id(id)
{:ok, resource}
end
end
There are two functions that you are required to implement, subject_for_token/2
and resource_for_claims/2
. The compiler will complain if they are not implemented.
The resource
refers to a user, and the sub
(subject) is the user's primary key (or another unique identifier). The function MyApp.get_resource_by_id(id)
, that is called in resource_from_claims/2
, is just a placeholder. You will need to implement that function (the name is not important, it can be whatever you want) and it should retrieve the user based on the sub
(subject).
- This is taken directly from Guardian's example code.
- More can be learned about claims by reading the JSON Web Token documentation
Finally, we add a config to config.exs
config :my_app, MyAppWeb.Guardian,
issuer: "my_app",
secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one"
You will need to create a secret key using the method above. If you're writing a production application, you should use an environment variable.
- The atom
:my_app
corresponds to the atom in the implementation module and the namespacing of the module also corresponds to the implementation module. - The value for the key
issuer
can be whatever you want.
Authentication
Our next step will be to perform the authentication of the credentials that the client has sent to our API.
def authenticate(%{user: user, password: password}) do
# Does password match the one stored in the database?
case Comeonin.Bcrypt.checkpw(password, user.password_digest) do
true ->
# Yes, create and return the token
MyAppWebWeb.Guardian.encode_and_sign(user)
_ ->
# No, return an error
{:error, :unauthorized}
end
end
This is my authentication function, it takes the User struct that I located in the database based on the unique identifier that was passed by the request (in my case can be either an email address or a username) and the password attempt.
I am using the password hashing library Comeonin to hash my passwords, so here I use the checkpw/2
function to check the password attempt against the digest (AKA a hash) stored in the database. This function returns either true or false, so if the password is correct, we fall into the true case and we create our token (JSON Web Token, or JWT) and return it.
Otherwise we return the tuple {:error, :unauthorized}
to signify that the authentication attempt failed.
The Controller
Let's expose this functionality to our public API by making a controller endpoint to sign in a user.
def sign_in(conn, params) do
# Find the user in the database based on the credentials sent with the request
with %User{} = user <- Accounts.find(params.email) do
# Attempt to authenticate the user
with {:ok, token, _claims} <- Accounts.authenticate(%{user: user, password: login_cred.password}) do
# Render the token
render conn, "token.json", token: token
end
end
end
Here we find the user in the database and authenticate the user. If the authentication is successful, we render the token back to the consumer.
Note: Here I am using the with
syntax along with Phoenix's action_fallback
functionality.
Authorization
In the context of a web application, this is the process of fencing off most of the routes from unauthenticated visitors. However, there are two routes that should not be fenced off, the route to sign in a user and the route to create a user.
# router.ex
pipeline :api do
plug :accepts, ["json"]
end
pipeline :api_auth do
plug MyAppWeb.Guardian.AuthPipeline
end
scope "/api", MyAppWeb.Api do
pipe_through :api
resources "/users", UserController, only: [:create]
post "/users/sign_in", UserController, :sign_in
end
scope "/api", MyAppWeb.Api do
pipe_through [:api, :api_auth]
resources "/users", UserController, only: [:update, :show, :delete]
end
For those unfamiliar with pipelines
, please reference the Phoenix guides.
We define a new pipeline called api_auth
for routes that require authorization, which in my case will be all routes except for UserController#sign_in
and UserController#create
.
Phoenix lets us define the same scopes multiple times without overwriting them. Here I define the api
and v1
scopes twice, piping the first only through the api
pipeline and piping the second through the api
and api_auth
pipelines.
The api_auth
pipeline consists of a custom plug I defined in /lib/my_app_web/auth/auth_pipeline.ex
.
# /lib/my_app_web/auth/auth_pipeline.ex
defmodule MyAppWeb.Guardian.AuthPipeline do
use Guardian.Plug.Pipeline, otp_app: :my_app,
module: MyAppWeb.Guardian,
error_handler: MyAppWeb.Guardian.AuthErrorHandler
plug Guardian.Plug.VerifyHeader
plug Guardian.Plug.EnsureAuthenticated
end
The first line of this plug defines some boiler plate that Guardian will use. The last two lines are what we want to check the authorization of the user. VerifyHeader
will look for the the Authorization header in the request, which should contain Bearer <your token>
, and EnsureAuthenticated
will make sure that the token is valid.
In the first line of boiler plate, we defined an error handler, MyAppWeb.Guardian.AuthErrorHandler
, mine consists of the example code from the Guardian documentation.
# /lib/my_app_web/auth/auth_error_handler.ex
defmodule MyAppWeb.Guardian.AuthErrorHandler do
import Plug.Conn
def auth_error(conn, {type, _reason}, _opts) do
body = Poison.encode!(%{message: to_string(type)})
send_resp(conn, 401, body)
end
end
Testing
At first I was unsure of how to test this functionality, but the solution turned out to be rather simple.
I've organized my test file to have two describe
blocks, one for tests that require authentication and one for tests that don't require it. I have a top level setup
block that adds the headers to the request that are common to all tests.
In the describe block for the tests requiring authentication, I wrote another setup
block.
setup %{conn: conn} do
# create a user
user = insert(:user, email: "user@email.com", username: "user")
# create the token
{:ok, token, _claims} = MyAppWeb.Guardian.encode_and_sign(user)
# add authorization header to request
conn = conn |> put_req_header("authorization", "Bearer #{token}")
# pass the connection and the user to the test
{:ok, conn: conn, user: user}
end
Any requests you make in your tests should now have the appropriate headers!
- I am using the ExUnit testing framework (comes with Elixir).
If you found this helpful, please let me know! You can find me on twitter as @mitchhanberg or you can shoot me an email.