View Source API Authentication
Requirement: This guide expects that you have gone through the
mix phx.gen.auth
guide.
This guide shows how to add API authentication on top of mix phx.gen.auth
. Since the authentication generator already includes a token table, we use it to store API tokens too, following the best security practices.
We will break this guide in two parts: augmenting the context and the plug implementation. We will assume that the following mix phx.gen.auth
command was executed:
$ mix phx.gen.auth Accounts User users
If you ran something else, it should be trivial to adapt the names.
Adding API functions to the context
Our authentication system will require two functions. One to create the API token and another to verify it. Open up lib/my_app/accounts.ex
and add these two new functions:
## API
@doc """
Creates a new api token for a user.
The token returned must be saved somewhere safe.
This token cannot be recovered from the database.
"""
def create_user_api_token(user) do
{encoded_token, user_token} = UserToken.build_email_token(user, "api-token")
Repo.insert!(user_token)
encoded_token
end
@doc """
Fetches the user by API token.
"""
def fetch_user_by_api_token(token) do
with {:ok, query} <- UserToken.verify_email_token_query(token, "api-token"),
%User{} = user <- Repo.one(query) do
{:ok, user}
else
_ -> :error
end
end
The new functions use the existing UserToken
functionality to store a new type of token called "api-token". Because this is an email token, if the user changes their email, the tokens will be expired.
Also notice we called the second function fetch_user_by_api_token
, instead of get_user_by_api_token
. Because we want to render different status codes in our API, depending if a user was found or not, we return {:ok, user}
or :error
. Elixir's convention is to call these functions fetch_*
, instead of get_*
which would usually return nil
instead of tuples.
To make sure our new functions work, let's write tests. Open up test/my_app/accounts_test.exs
and add this new describe block:
describe "create_user_api_token/1 and fetch_user_by_api_token/1" do
test "creates and fetches by token" do
user = user_fixture()
token = Accounts.create_user_api_token(user)
assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
assert Accounts.fetch_user_by_api_token("invalid") == :error
end
end
If you run the tests, they will actually fail. Something similar to this:
1) test create_user_api_token/1 and fetch_user_by_api_token/1 creates and verify token (Demo.AccountsTest)
test/demo/accounts_test.exs:21
** (FunctionClauseError) no function clause matching in Demo.Accounts.UserToken.days_for_context/1
The following arguments were given to Demo.Accounts.UserToken.days_for_context/1:
# 1
"api-token"
Attempted function clauses (showing 2 out of 2):
defp days_for_context("confirm")
defp days_for_context("reset_password")
code: assert Accounts.verify_api_token(token) == {:ok, user}
stacktrace:
(demo 0.1.0) lib/demo/accounts/user_token.ex:129: Demo.Accounts.UserToken.days_for_context/1
(demo 0.1.0) lib/demo/accounts/user_token.ex:114: Demo.Accounts.UserToken.verify_email_token_query/2
(demo 0.1.0) lib/demo/accounts.ex:301: Demo.Accounts.verify_api_token/1
test/demo/accounts_test.exs:24: (test)
If you prefer, try looking at the error and fixing it yourself. The explanation will come next.
The UserToken
module expects us to declare the validity of each token and we haven't defined one for "api-token". The length is going to depend on your application and how sensitive it is in terms of security. For this example, let's say the token is valid for 365 days.
Open up lib/my_app/accounts/user_token.ex
, find where defp days_for_context
is defined, and add a new clause, like this:
defp days_for_context("api-token"), do: 365
defp days_for_context("confirm"), do: @confirm_validity_in_days
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
Now tests should pass and we are ready to move forward!
API authentication plug
The last part is to add authentication to our API.
When we ran mix phx.gen.auth
, it generated a MyAppWeb.UserAuth
module with several plugs, which are small functions that receive the conn
and customize our request/response life-cycle. Open up lib/my_app_web/user_auth.ex
and add this new function:
def fetch_api_user(conn, _opts) do
with ["Bearer " <> token] <- get_req_header("authorization"),
{:ok, user} <- Accounts.fetch_user_by_api_token(token) do
assign(conn, :current_user, user)
else
_ ->
conn
|> send_resp(:unauthorized, "No access for you")
|> halt()
end
end
Our function receives the connection and checks if the "authorization" header has been set with "Bearer TOKEN", where "TOKEN" is the value returned by Accounts.create_user_api_token/1
. In case the token is not valid or there is no such user, we abort the request.
Finally, we need to add this plug
to our pipeline. Open up lib/my_app_web/router.ex
and you will find a pipeline for API. Let's add our new plug under it, like this:
pipeline :api do
plug :accepts, ["json"]
plug :fetch_api_user
end
Now you are ready to receive and validate API requests. Feel free to open up test/my_app_web/user_auth_test.exs
and write your own test. You can use the tests for other plugs as templates!
Your turn
The overall API authentication flow will depend on your application.
If you want to use this token in a JavaScript client, you will need to slightly alter the UserSessionController
to invoke Accounts.create_user_api_token/1
and return a JSON response and include the token returned it.
If you want to provide APIs for 3rd-party users, you will need to allow them to create tokens, and show the result of Accounts.create_user_api_token/1
to them. They must save these tokens somewhere safe and include them as part of their requests using the "authorization" header.