or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

code-generation.mdindex.mdpresence.mdreal-time.mdsecurity.mdtesting.mdweb-foundation.md

security.mddocs/

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

```