Phoenix (Elixir)MiddleTechnical
Как реализовать аутентификацию в приложении Phoenix?
Phoenix аутентификацию реализуют через mix phx.gen.auth (встроенный генератор): создаёт User-схему, сессионные токены, plug-пайплайн и LiveView-формы. Для JWT используют библиотеки Joken или Guardian.
Встроенный генератор mix phx.gen.auth
Начиная с Phoenix 1.6, mix phx.gen.auth генерирует полную систему аутентификации с сессионными токенами, хэшированием паролей через bcrypt, подтверждением email и восстановлением пароля.
# Генерация модуля аутентификации
mix phx.gen.auth Accounts User users
# Применяем миграцию
mix ecto.migrate
Генератор создаёт:
lib/myapp/accounts/user.ex— Ecto schemalib/myapp/accounts/user_token.ex— токены сессийlib/myapp_web/user_auth.ex— plug-хелперыlib/myapp_web/controllers/user_session_controller.ex- Миграции для таблиц users и users_tokens
Структура User schema
# lib/myapp/accounts/user.ex (сгенерированный)
defmodule Myapp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
timestamps(type: :utc_datetime)
end
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password])
|> validate_email(opts)
|> validate_password(opts)
end
defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
|> maybe_hash_password(opts)
end
defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)
if hash_password? && password && changeset.valid? do
changeset
|> validate_length(:password, max: 72, count: :bytes)
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
end
Plug-пайплайн аутентификации
# lib/myapp_web/user_auth.ex — ключевые функции
defmodule MyappWeb.UserAuth do
import Plug.Conn
import Phoenix.Controller
alias Myapp.Accounts
# Создание сессии после логина
def log_in_user(conn, user, params \\ %{}) do
token = Accounts.generate_user_session_token(user)
user_return_to = get_session(conn, :user_return_to)
conn
|> renew_session() # Защита от session fixation
|> put_token_in_session(token)
|> redirect(to: user_return_to || ~p"/")
end
# Plug для защищённых маршрутов
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/users/log_in")
|> halt()
end
end
end
Подключение plugs в Router
# lib/myapp_web/router.ex
defmodule MyappWeb.Router do
use MyappWeb, :router
import MyappWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyappWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user # загружает current_user из сессии
end
# Публичные маршруты
scope "/", MyappWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
end
# Защищённые маршруты
scope "/", MyappWeb do
pipe_through [:browser, :require_authenticated_user]
get "/dashboard", DashboardController, :index
delete "/users/log_out", UserSessionController, :delete
end
end
JWT-аутентификация с Joken
# mix.exs
{:joken, "~> 2.6"}
# lib/myapp/auth/jwt.ex
defmodule Myapp.Auth.JWT do
use Joken.Config
@impl Joken.Config
def token_config do
default_claims(iss: "myapp", aud: "myapp-api")
|> add_claim("role", nil, &(&1 in ["admin", "user"]))
end
def generate_token(user_id, role) do
claims = %{"sub" => to_string(user_id), "role" => role}
generate_and_sign!(claims)
end
def verify_token(token) do
verify_and_validate(token)
end
end
Подводные камни
- renew_session обязателен: без
renew_session()при логине сессия не ротируется, что открывает Session Fixation атаку. - bcrypt_elixir требует libsodium: в Docker-образе нужно устанавливать системные зависимости (
libsodium-dev) или использоватьcomeoninсpbkdf2_elixir. - Токены не инвалидируются автоматически:
phx.gen.authиспользует DB-токены, но при компрометации секрета старые токены остаются валидными до срока истечения. Реализуйтеlog_out_userчерез удаление токена из БД. - LiveView и аутентификация:
current_userнужно передавать черезon_mountхук в LiveView, иначе защищённые live-роуты будут доступны без авторизации. - CSRF в LiveView: LiveView использует WebSocket, не HTTP. CSRF-токен из формы валидируется только при начальном HTTP-запросе; дальнейшие events защищены самим WebSocket-соединением.
- Secrets в конфиге: JWT-секрет должен быть в
runtime.exs, не вconfig.exs, иначе он вшивается в релиз на этапе компиляции.
Common mistakes
- Сводить authentication к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Phoenix 1.8 работает поверх Plug, Endpoint, Router, Controllers/LiveViews, PubSub и OTP supervision.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет authentication через конкретную точку lifecycle в Phoenix (Elixir).
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
- Идёт от входных данных к БД/кешу/логам, а не предлагает случайные исправления.