0
# Security and Authentication
1
2
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.
3
4
## Token-based Authentication
5
6
Phoenix.Token provides cryptographically secure token generation and verification for authentication and data protection.
7
8
### Phoenix.Token
9
10
```elixir { .api }
11
defmodule Phoenix.Token do
12
# Signing functions
13
def sign(context, salt, data, opts \\ []) :: binary
14
def verify(context, salt, token, opts \\ []) ::
15
{:ok, term} | {:error, :invalid | :expired}
16
17
# Encryption functions
18
def encrypt(context, secret, data, opts \\ []) :: binary
19
def decrypt(context, secret, token, opts \\ []) ::
20
{:ok, term} | {:error, :invalid | :expired}
21
end
22
23
# Context types
24
@type context ::
25
Phoenix.Endpoint.t() |
26
Plug.Conn.t() |
27
Phoenix.Socket.t() |
28
binary
29
30
# Token options
31
@type token_opts :: [
32
max_age: pos_integer, # Token expiration in seconds
33
key_iterations: pos_integer, # PBKDF2 iterations (default: 1000)
34
key_length: pos_integer, # Derived key length (default: 32)
35
key_digest: atom # Hash algorithm (default: :sha256)
36
]
37
```
38
39
### Usage Examples
40
41
```elixir
42
# User authentication tokens
43
defmodule MyAppWeb.Auth do
44
@salt "user_auth"
45
@max_age 86400 # 24 hours
46
47
def generate_user_token(user_id) do
48
Phoenix.Token.sign(MyAppWeb.Endpoint, @salt, user_id, max_age: @max_age)
49
end
50
51
def verify_user_token(token) do
52
case Phoenix.Token.verify(MyAppWeb.Endpoint, @salt, token, max_age: @max_age) do
53
{:ok, user_id} ->
54
case MyApp.Accounts.get_user(user_id) do
55
nil -> {:error, :user_not_found}
56
user -> {:ok, user}
57
end
58
59
error ->
60
error
61
end
62
end
63
end
64
65
# Password reset tokens
66
defmodule MyApp.Accounts do
67
@reset_salt "password_reset"
68
@reset_max_age 3600 # 1 hour
69
70
def generate_password_reset_token(user) do
71
Phoenix.Token.sign(MyAppWeb.Endpoint, @reset_salt, user.id)
72
end
73
74
def verify_password_reset_token(token) do
75
Phoenix.Token.verify(
76
MyAppWeb.Endpoint,
77
@reset_salt,
78
token,
79
max_age: @reset_max_age
80
)
81
end
82
end
83
84
# Email verification tokens
85
defmodule MyApp.Accounts do
86
@email_salt "email_verification"
87
88
def generate_email_verification_token(user) do
89
Phoenix.Token.encrypt(
90
MyAppWeb.Endpoint,
91
@email_salt,
92
%{user_id: user.id, email: user.email}
93
)
94
end
95
96
def verify_email_token(token) do
97
Phoenix.Token.decrypt(MyAppWeb.Endpoint, @email_salt, token)
98
end
99
end
100
```
101
102
## CSRF Protection
103
104
Phoenix includes built-in CSRF (Cross-Site Request Forgery) protection for web applications.
105
106
### CSRF Functions
107
108
```elixir { .api }
109
# From Phoenix.Controller
110
def protect_from_forgery(Plug.Conn.t(), keyword) :: Plug.Conn.t()
111
112
# CSRF options
113
@type csrf_opts :: [
114
session_key: binary, # Session key for CSRF token
115
cookie_key: binary, # Cookie key for CSRF token
116
with: (Plug.Conn.t(), binary -> Plug.Conn.t()) # Custom error handler
117
]
118
```
119
120
### Usage Examples
121
122
```elixir
123
# In router pipelines
124
defmodule MyAppWeb.Router do
125
use Phoenix.Router
126
127
pipeline :browser do
128
plug :accepts, ["html"]
129
plug :fetch_session
130
plug :fetch_live_flash
131
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
132
plug :protect_from_forgery
133
plug :put_secure_browser_headers
134
end
135
end
136
137
# Custom CSRF error handling
138
pipeline :browser do
139
plug :protect_from_forgery, with: &MyAppWeb.CSRFHandler.handle_csrf_error/2
140
end
141
142
defmodule MyAppWeb.CSRFHandler do
143
def handle_csrf_error(conn, _reason) do
144
conn
145
|> Phoenix.Controller.put_flash(:error, "Invalid security token")
146
|> Phoenix.Controller.redirect(to: "/")
147
|> Plug.Conn.halt()
148
end
149
end
150
151
# In forms (templates)
152
<%= form_for @changeset, @action, fn f -> %>
153
<%= csrf_token_tag() %>
154
<%= text_input f, :name %>
155
<%= submit "Save" %>
156
<% end %>
157
```
158
159
## Secure Headers
160
161
Phoenix provides utilities for setting security-related HTTP headers.
162
163
### Security Headers
164
165
```elixir { .api }
166
# From Phoenix.Controller
167
def put_secure_browser_headers(Plug.Conn.t(), map) :: Plug.Conn.t()
168
169
# Default secure headers
170
@default_headers %{
171
"content-security-policy" => "default-src 'self'",
172
"cross-origin-window-policy" => "deny",
173
"permissions-policy" => "camera=(), microphone=(), geolocation=()",
174
"referrer-policy" => "strict-origin-when-cross-origin",
175
"x-content-type-options" => "nosniff",
176
"x-download-options" => "noopen",
177
"x-frame-options" => "SAMEORIGIN",
178
"x-permitted-cross-domain-policies" => "none",
179
"x-xss-protection" => "1; mode=block"
180
}
181
```
182
183
### Usage Examples
184
185
```elixir
186
# Default secure headers
187
def index(conn, _params) do
188
conn = put_secure_browser_headers(conn)
189
render(conn, "index.html")
190
end
191
192
# Custom secure headers
193
def api_endpoint(conn, _params) do
194
headers = %{
195
"content-security-policy" => "default-src 'none'",
196
"x-content-type-options" => "nosniff",
197
"access-control-allow-origin" => "https://example.com"
198
}
199
200
conn = put_secure_browser_headers(conn, headers)
201
json(conn, %{data: "secure response"})
202
end
203
204
# In router pipeline
205
pipeline :secure_api do
206
plug :accepts, ["json"]
207
plug :put_secure_browser_headers, %{
208
"strict-transport-security" => "max-age=31536000"
209
}
210
end
211
```
212
213
## Parameter Filtering and Scrubbing
214
215
Phoenix provides utilities for filtering sensitive parameters from logs and scrubbing empty parameters.
216
217
### Parameter Security
218
219
```elixir { .api }
220
# From Phoenix.Controller
221
def scrub_params(Plug.Conn.t(), binary) :: Plug.Conn.t()
222
223
# From Phoenix.Logger
224
def compile_filter([binary | atom]) :: (map, map -> map)
225
```
226
227
### Usage Examples
228
229
```elixir
230
# Scrub empty parameters
231
defmodule MyAppWeb.UserController do
232
use Phoenix.Controller
233
234
plug :scrub_params, "user" when action in [:create, :update]
235
236
def create(conn, %{"user" => user_params}) do
237
# Empty strings are now converted to nil
238
case MyApp.Accounts.create_user(user_params) do
239
{:ok, user} -> redirect(conn, to: "/users/#{user.id}")
240
{:error, changeset} -> render(conn, "new.html", changeset: changeset)
241
end
242
end
243
end
244
245
# Configure parameter filtering
246
# config/config.exs
247
config :phoenix, :filter_parameters, ["password", "secret", "token", "api_key"]
248
249
# Custom parameter filtering
250
config :phoenix, :filter_parameters, [
251
"password",
252
"secret",
253
~r/.*_token$/,
254
fn key, value ->
255
case key do
256
"credit_card" -> "[REDACTED]"
257
_ -> value
258
end
259
end
260
]
261
```
262
263
## Authentication Plugs
264
265
Custom authentication plugs for securing routes and resources.
266
267
### Authentication Patterns
268
269
```elixir
270
# Authentication plug
271
defmodule MyAppWeb.Auth do
272
import Plug.Conn
273
import Phoenix.Controller
274
alias MyApp.Accounts
275
276
def init(opts), do: opts
277
278
def call(conn, _opts) do
279
user_id = get_session(conn, :user_id)
280
281
cond do
282
user = user_id && Accounts.get_user(user_id) ->
283
assign(conn, :current_user, user)
284
285
true ->
286
assign(conn, :current_user, nil)
287
end
288
end
289
290
def login(conn, user) do
291
conn
292
|> assign(:current_user, user)
293
|> put_session(:user_id, user.id)
294
|> configure_session(renew: true)
295
end
296
297
def logout(conn) do
298
configure_session(conn, drop: true)
299
end
300
301
def authenticate_user(conn, _opts) do
302
if conn.assigns[:current_user] do
303
conn
304
else
305
conn
306
|> put_flash(:error, "You must be logged in")
307
|> redirect(to: "/login")
308
|> halt()
309
end
310
end
311
end
312
313
# Usage in router
314
pipeline :auth do
315
plug MyAppWeb.Auth
316
end
317
318
pipeline :ensure_auth do
319
plug MyAppWeb.Auth
320
plug MyAppWeb.Auth, :authenticate_user
321
end
322
323
scope "/", MyAppWeb do
324
pipe_through [:browser, :auth]
325
# Public routes with user info
326
end
327
328
scope "/admin", MyAppWeb do
329
pipe_through [:browser, :ensure_auth]
330
# Protected admin routes
331
end
332
```
333
334
## Socket Authentication
335
336
Secure WebSocket connections using token-based authentication.
337
338
### Socket Security
339
340
```elixir
341
defmodule MyAppWeb.UserSocket do
342
use Phoenix.Socket
343
344
@token_salt "socket_auth"
345
@token_max_age 86400 # 24 hours
346
347
def connect(%{"token" => token}, socket, _connect_info) do
348
case Phoenix.Token.verify(
349
MyAppWeb.Endpoint,
350
@token_salt,
351
token,
352
max_age: @token_max_age
353
) do
354
{:ok, user_id} ->
355
case MyApp.Accounts.get_user(user_id) do
356
nil -> :error
357
user -> {:ok, assign(socket, :user, user)}
358
end
359
360
{:error, _reason} ->
361
:error
362
end
363
end
364
365
def connect(_params, _socket, _connect_info), do: :error
366
367
def id(socket), do: "user:#{socket.assigns.user.id}"
368
369
# Generate tokens for clients
370
def generate_socket_token(user_id) do
371
Phoenix.Token.sign(MyAppWeb.Endpoint, @token_salt, user_id)
372
end
373
end
374
375
# Channel authorization
376
defmodule MyAppWeb.PrivateChannel do
377
use Phoenix.Channel
378
379
def join("private:" <> user_id, _params, socket) do
380
if socket.assigns.user.id == String.to_integer(user_id) do
381
{:ok, socket}
382
else
383
{:error, %{reason: "unauthorized"}}
384
end
385
end
386
end
387
```
388
389
## Rate Limiting and Throttling
390
391
Implement rate limiting for API endpoints and security-sensitive operations.
392
393
```elixir
394
# Custom rate limiting plug
395
defmodule MyAppWeb.RateLimiter do
396
import Plug.Conn
397
import Phoenix.Controller
398
399
def init(opts) do
400
Keyword.merge([max_requests: 100, window_ms: 60_000], opts)
401
end
402
403
def call(conn, opts) do
404
key = get_rate_limit_key(conn)
405
max_requests = opts[:max_requests]
406
window_ms = opts[:window_ms]
407
408
case check_rate_limit(key, max_requests, window_ms) do
409
:ok ->
410
conn
411
412
{:error, :rate_limited} ->
413
conn
414
|> put_status(:too_many_requests)
415
|> json(%{error: "Rate limit exceeded"})
416
|> halt()
417
end
418
end
419
420
defp get_rate_limit_key(conn) do
421
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
422
user_id = get_session(conn, :user_id) || "anonymous"
423
"rate_limit:#{user_id}:#{ip}"
424
end
425
426
defp check_rate_limit(key, max_requests, window_ms) do
427
# Implementation using ETS, Redis, or other storage
428
# Return :ok or {:error, :rate_limited}
429
end
430
end
431
432
# Usage in routes
433
pipeline :api_limited do
434
plug :accepts, ["json"]
435
plug MyAppWeb.RateLimiter, max_requests: 1000, window_ms: 3600_000
436
end
437
```
438
439
## Input Validation and Sanitization
440
441
Secure input handling and validation patterns.
442
443
```elixir
444
# Input validation
445
defmodule MyApp.Accounts.User do
446
use Ecto.Schema
447
import Ecto.Changeset
448
449
schema "users" do
450
field :email, :string
451
field :password, :string, virtual: true
452
field :password_hash, :string
453
field :name, :string
454
455
timestamps()
456
end
457
458
def registration_changeset(user, attrs) do
459
user
460
|> cast(attrs, [:email, :password, :name])
461
|> validate_required([:email, :password, :name])
462
|> validate_format(:email, ~r/@/)
463
|> validate_length(:password, min: 8, max: 100)
464
|> validate_length(:name, min: 1, max: 100)
465
|> unique_constraint(:email)
466
|> hash_password()
467
end
468
469
defp hash_password(%{valid?: true, changes: %{password: password}} = changeset) do
470
put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password))
471
end
472
473
defp hash_password(changeset), do: changeset
474
end
475
476
# HTML sanitization
477
defmodule MyApp.Utils.Sanitizer do
478
def sanitize_html(html) when is_binary(html) do
479
HtmlSanitizeEx.strip_tags(html)
480
end
481
482
def sanitize_html(_), do: ""
483
484
def safe_html(html) when is_binary(html) do
485
HtmlSanitizeEx.basic_html(html)
486
end
487
end
488
```