Аутентификация API
Исходный кодТребование: предполагается, что вы ознакомились с руководством
mix phx.gen.auth.
В этом руководстве показано, как добавить аутентификацию API поверх mix phx.gen.auth. Поскольку генератор аутентификации уже добавляет таблицу токенов, мы используем её и для хранения токенов API, следуя лучшим практикам безопасности.
Руководство состоит из двух частей: расширение контекста и реализация plug. Предполагается, что была выполнена следующая команда mix phx.gen.auth:
$ mix phx.gen.auth Accounts User users
Если вы выполнили другую команду, адаптировать имена будет несложно.
Добавление функций API в контекст
Для нашей системы аутентификации потребуются две функции: одна для создания токена API, а другая — для его проверки. Откройте lib/my_app/accounts.ex и добавьте эти две новые функции:
## 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_api_token_query(token),
%User{} = user <- Repo.one(query) do
{:ok, user}
else
_ -> :error
end
end
Новые функции используют существующую функциональность UserToken для сохранения нового типа токена под названием "api-token". Поскольку это токен электронной почты, при смене адреса электронной почты пользователя срок действия токенов истечёт.
Обратите внимание, что мы назвали вторую функцию fetch_user_by_api_token, а не get_user_by_api_token. Поскольку в нашем API мы хотим возвращать разные коды состояния в зависимости от того, найден пользователь или нет, функция возвращает {:ok, user} или :error. По соглашению в Elixir такие функции называют fetch_*, а не get_*, которая обычно возвращала бы nil вместо кортежей.
Чтобы убедиться, что новые функции работают, напишем тесты. Откройте test/my_app/accounts_test.exs и добавьте следующий блок describe:
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
Если запустить тесты, они завершатся с ошибкой. Например, такой:
1) test create_user_api_token/1 and fetch_user_by_api_token/1 creates and fetches by token (Demo.AccountsTest)
test/demo/accounts_test.exs:380
** (UndefinedFunctionError) function Demo.Accounts.UserToken.verify_api_token_query/1 is undefined or private. Did you mean:
* verify_change_email_token_query/2
* verify_magic_link_token_query/1
* verify_session_token_query/1
code: assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
stacktrace:
(demo 0.1.0) Demo.Accounts.UserToken.verify_api_token_query("sTpJg7rt-KQ9gZ7xLMtn2keusGk9N2JpPwkXDx7LmHU")
(demo 0.1.0) lib/demo/accounts.ex:325: Demo.Accounts.fetch_user_by_api_token/1
test/demo/accounts_test.exs:383: (test)
При желании попробуйте изучить ошибку и исправить её самостоятельно. Объяснение будет приведено далее.
Модуль UserToken содержит функции для проверки различных токенов. Сейчас функции verify_api_token_query/1 нет, но мы можем реализовать её по аналогии с уже существующими функциями. Срок действия токена API зависит от вашего приложения и от того, насколько оно чувствительно с точки зрения безопасности. В нашем примере будем считать, что токен действителен в течение 365 дней.
Откройте lib/my_app/accounts/user_token.ex и добавьте новую функцию, например, такую:
@doc """
Checks if the API token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any.
The given token is valid if it matches its hashed counterpart in the
database and the user email has not changed. This function also checks
if the token is being used within 365 days.
"""
def verify_api_token_query(token) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
from token in by_token_and_context_query(hashed_token, "api-token"),
join: user in assoc(token, :user),
where:
token.inserted_at > ago(^@api_token_validity_in_days, "day") and
token.sent_to == user.email,
select: user
{:ok, query}
:error ->
:error
end
end
Обратите внимание: в начале файла мы также добавили атрибут модуля @api_token_validity_in_days:
@magic_link_validity_in_minutes 15 @change_email_validity_in_days 7 @session_validity_in_days 60 + @api_token_validity_in_days 365
Теперь тесты должны пройти, и можно двигаться дальше!
Plug для аутентификации API
Осталось добавить аутентификацию в наш API.
При выполнении команды mix phx.gen.auth был создан модуль MyAppWeb.UserAuth с несколькими plug — небольшими функциями, которые получают conn и настраивают жизненный цикл запроса и ответа. Откройте lib/my_app_web/user_auth.ex и добавьте новую функцию:
def fetch_current_scope_for_api_user(conn, _opts) do
with [<<bearer::binary-size(6), " ", token::binary>>] <-
get_req_header(conn, "authorization"),
true <- String.downcase(bearer) == "bearer",
{:ok, user} <- Accounts.fetch_user_by_api_token(token) do
assign(conn, :current_scope, Scope.for_user(user))
else
_ ->
conn
|> send_resp(:unauthorized, "No access for you")
|> halt()
end
end
Функция получает соединение и проверяет, установлен ли заголовок "authorization" в формате "Bearer TOKEN", где "TOKEN" — это значение, возвращённое Accounts.create_user_api_token/1. Если токен недействителен или такого пользователя не существует, запрос прерывается.
Наконец, нужно добавить этот plug в наш конвейер. Откройте lib/my_app_web/router.ex — там вы найдёте конвейер для API. Добавим в него наш новый plug:
pipeline :api do
plug :accepts, ["json"]
plug :fetch_current_scope_for_api_user
end
Теперь можно принимать запросы API и проверять их. Откройте test/my_app_web/user_auth_test.exs и напишите собственный тест. В качестве шаблона можно использовать тесты для других plug!
Теперь ваша очередь
Конкретный процесс аутентификации API зависит от вашего приложения.
Если вы хотите использовать этот токен в клиенте JavaScript, нужно немного изменить UserSessionController, чтобы вызвать Accounts.create_user_api_token/1 и вернуть ответ JSON с токеном.
Если вы предоставляете API сторонним пользователям, нужно разрешить им создавать токены и показывать им результат Accounts.create_user_api_token/1. Они должны сохранить эти токены в безопасном месте и включать их в запросы с помощью заголовка "authorization".
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/api_authentication.html