Spec-Zone.ru › Phoenix

Аутентификация 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".

← Предыдущая страница Области видимости
Следующая страница → Каналы

Пакет Hex Предпросмотр Hex (текущий файл) Скачать версию ePub

Создано с помощью ExDoc (v0.40.3) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/api_authentication.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API