Peace of mind from prototype to production - comprehensive web framework for Elixir
Phoenix provides comprehensive security features including token-based authentication, CSRF protection, secure headers, and parameter filtering. The security system is designed to protect against common web vulnerabilities while maintaining flexibility for custom authentication schemes.
Phoenix.Token provides cryptographically secure token generation and verification for authentication and data protection.
defmodule Phoenix.Token do
# Signing functions
def sign(context, salt, data, opts \\ []) :: binary
def verify(context, salt, token, opts \\ []) ::
{:ok, term} | {:error, :invalid | :expired}
# Encryption functions
def encrypt(context, secret, data, opts \\ []) :: binary
def decrypt(context, secret, token, opts \\ []) ::
{:ok, term} | {:error, :invalid | :expired}
end
# Context types
@type context ::
Phoenix.Endpoint.t() |
Plug.Conn.t() |
Phoenix.Socket.t() |
binary
# Token options
@type token_opts :: [
max_age: pos_integer, # Token expiration in seconds
key_iterations: pos_integer, # PBKDF2 iterations (default: 1000)
key_length: pos_integer, # Derived key length (default: 32)
key_digest: atom # Hash algorithm (default: :sha256)
]# User authentication tokens
defmodule MyAppWeb.Auth do
@salt "user_auth"
@max_age 86400 # 24 hours
def generate_user_token(user_id) do
Phoenix.Token.sign(MyAppWeb.Endpoint, @salt, user_id, max_age: @max_age)
end
def verify_user_token(token) do
case Phoenix.Token.verify(MyAppWeb.Endpoint, @salt, token, max_age: @max_age) do
{:ok, user_id} ->
case MyApp.Accounts.get_user(user_id) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
end
error ->
error
end
end
end
# Password reset tokens
defmodule MyApp.Accounts do
@reset_salt "password_reset"
@reset_max_age 3600 # 1 hour
def generate_password_reset_token(user) do
Phoenix.Token.sign(MyAppWeb.Endpoint, @reset_salt, user.id)
end
def verify_password_reset_token(token) do
Phoenix.Token.verify(
MyAppWeb.Endpoint,
@reset_salt,
token,
max_age: @reset_max_age
)
end
end
# Email verification tokens
defmodule MyApp.Accounts do
@email_salt "email_verification"
def generate_email_verification_token(user) do
Phoenix.Token.encrypt(
MyAppWeb.Endpoint,
@email_salt,
%{user_id: user.id, email: user.email}
)
end
def verify_email_token(token) do
Phoenix.Token.decrypt(MyAppWeb.Endpoint, @email_salt, token)
end
endPhoenix includes built-in CSRF (Cross-Site Request Forgery) protection for web applications.
# From Phoenix.Controller
def protect_from_forgery(Plug.Conn.t(), keyword) :: Plug.Conn.t()
# CSRF options
@type csrf_opts :: [
session_key: binary, # Session key for CSRF token
cookie_key: binary, # Cookie key for CSRF token
with: (Plug.Conn.t(), binary -> Plug.Conn.t()) # Custom error handler
]# In router pipelines
defmodule MyAppWeb.Router do
use Phoenix.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
end
# Custom CSRF error handling
pipeline :browser do
plug :protect_from_forgery, with: &MyAppWeb.CSRFHandler.handle_csrf_error/2
end
defmodule MyAppWeb.CSRFHandler do
def handle_csrf_error(conn, _reason) do
conn
|> Phoenix.Controller.put_flash(:error, "Invalid security token")
|> Phoenix.Controller.redirect(to: "/")
|> Plug.Conn.halt()
end
end
# In forms (templates)
<%= form_for @changeset, @action, fn f -> %>
<%= csrf_token_tag() %>
<%= text_input f, :name %>
<%= submit "Save" %>
<% end %>Phoenix provides utilities for setting security-related HTTP headers.
# From Phoenix.Controller
def put_secure_browser_headers(Plug.Conn.t(), map) :: Plug.Conn.t()
# Default secure headers
@default_headers %{
"content-security-policy" => "default-src 'self'",
"cross-origin-window-policy" => "deny",
"permissions-policy" => "camera=(), microphone=(), geolocation=()",
"referrer-policy" => "strict-origin-when-cross-origin",
"x-content-type-options" => "nosniff",
"x-download-options" => "noopen",
"x-frame-options" => "SAMEORIGIN",
"x-permitted-cross-domain-policies" => "none",
"x-xss-protection" => "1; mode=block"
}# Default secure headers
def index(conn, _params) do
conn = put_secure_browser_headers(conn)
render(conn, "index.html")
end
# Custom secure headers
def api_endpoint(conn, _params) do
headers = %{
"content-security-policy" => "default-src 'none'",
"x-content-type-options" => "nosniff",
"access-control-allow-origin" => "https://example.com"
}
conn = put_secure_browser_headers(conn, headers)
json(conn, %{data: "secure response"})
end
# In router pipeline
pipeline :secure_api do
plug :accepts, ["json"]
plug :put_secure_browser_headers, %{
"strict-transport-security" => "max-age=31536000"
}
endPhoenix provides utilities for filtering sensitive parameters from logs and scrubbing empty parameters.
# From Phoenix.Controller
def scrub_params(Plug.Conn.t(), binary) :: Plug.Conn.t()
# From Phoenix.Logger
def compile_filter([binary | atom]) :: (map, map -> map)# Scrub empty parameters
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug :scrub_params, "user" when action in [:create, :update]
def create(conn, %{"user" => user_params}) do
# Empty strings are now converted to nil
case MyApp.Accounts.create_user(user_params) do
{:ok, user} -> redirect(conn, to: "/users/#{user.id}")
{:error, changeset} -> render(conn, "new.html", changeset: changeset)
end
end
end
# Configure parameter filtering
# config/config.exs
config :phoenix, :filter_parameters, ["password", "secret", "token", "api_key"]
# Custom parameter filtering
config :phoenix, :filter_parameters, [
"password",
"secret",
~r/.*_token$/,
fn key, value ->
case key do
"credit_card" -> "[REDACTED]"
_ -> value
end
end
]Custom authentication plugs for securing routes and resources.
# Authentication plug
defmodule MyAppWeb.Auth do
import Plug.Conn
import Phoenix.Controller
alias MyApp.Accounts
def init(opts), do: opts
def call(conn, _opts) do
user_id = get_session(conn, :user_id)
cond do
user = user_id && Accounts.get_user(user_id) ->
assign(conn, :current_user, user)
true ->
assign(conn, :current_user, nil)
end
end
def login(conn, user) do
conn
|> assign(:current_user, user)
|> put_session(:user_id, user.id)
|> configure_session(renew: true)
end
def logout(conn) do
configure_session(conn, drop: true)
end
def authenticate_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must be logged in")
|> redirect(to: "/login")
|> halt()
end
end
end
# Usage in router
pipeline :auth do
plug MyAppWeb.Auth
end
pipeline :ensure_auth do
plug MyAppWeb.Auth
plug MyAppWeb.Auth, :authenticate_user
end
scope "/", MyAppWeb do
pipe_through [:browser, :auth]
# Public routes with user info
end
scope "/admin", MyAppWeb do
pipe_through [:browser, :ensure_auth]
# Protected admin routes
endSecure WebSocket connections using token-based authentication.
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
@token_salt "socket_auth"
@token_max_age 86400 # 24 hours
def connect(%{"token" => token}, socket, _connect_info) do
case Phoenix.Token.verify(
MyAppWeb.Endpoint,
@token_salt,
token,
max_age: @token_max_age
) do
{:ok, user_id} ->
case MyApp.Accounts.get_user(user_id) do
nil -> :error
user -> {:ok, assign(socket, :user, user)}
end
{:error, _reason} ->
:error
end
end
def connect(_params, _socket, _connect_info), do: :error
def id(socket), do: "user:#{socket.assigns.user.id}"
# Generate tokens for clients
def generate_socket_token(user_id) do
Phoenix.Token.sign(MyAppWeb.Endpoint, @token_salt, user_id)
end
end
# Channel authorization
defmodule MyAppWeb.PrivateChannel do
use Phoenix.Channel
def join("private:" <> user_id, _params, socket) do
if socket.assigns.user.id == String.to_integer(user_id) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
endImplement rate limiting for API endpoints and security-sensitive operations.
# Custom rate limiting plug
defmodule MyAppWeb.RateLimiter do
import Plug.Conn
import Phoenix.Controller
def init(opts) do
Keyword.merge([max_requests: 100, window_ms: 60_000], opts)
end
def call(conn, opts) do
key = get_rate_limit_key(conn)
max_requests = opts[:max_requests]
window_ms = opts[:window_ms]
case check_rate_limit(key, max_requests, window_ms) do
:ok ->
conn
{:error, :rate_limited} ->
conn
|> put_status(:too_many_requests)
|> json(%{error: "Rate limit exceeded"})
|> halt()
end
end
defp get_rate_limit_key(conn) do
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
user_id = get_session(conn, :user_id) || "anonymous"
"rate_limit:#{user_id}:#{ip}"
end
defp check_rate_limit(key, max_requests, window_ms) do
# Implementation using ETS, Redis, or other storage
# Return :ok or {:error, :rate_limited}
end
end
# Usage in routes
pipeline :api_limited do
plug :accepts, ["json"]
plug MyAppWeb.RateLimiter, max_requests: 1000, window_ms: 3600_000
endSecure input handling and validation patterns.
# Input validation
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true
field :password_hash, :string
field :name, :string
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password, :name])
|> validate_required([:email, :password, :name])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 8, max: 100)
|> validate_length(:name, min: 1, max: 100)
|> unique_constraint(:email)
|> hash_password()
end
defp hash_password(%{valid?: true, changes: %{password: password}} = changeset) do
put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password))
end
defp hash_password(changeset), do: changeset
end
# HTML sanitization
defmodule MyApp.Utils.Sanitizer do
def sanitize_html(html) when is_binary(html) do
HtmlSanitizeEx.strip_tags(html)
end
def sanitize_html(_), do: ""
def safe_html(html) when is_binary(html) do
HtmlSanitizeEx.basic_html(html)
end
endInstall with Tessl CLI
npx tessl i tessl/hex-phoenix