DEV Community

Cover image for Locking the Gate: Secure and Scalable API Authentication in Phoenix
HexShift
HexShift

Posted on

Locking the Gate: Secure and Scalable API Authentication in Phoenix

Every API starts with trust.

And trust is earned at the boundary.

You can build fast endpoints. Return beautiful JSON. Document your data contracts.

But if anyone can call it without proving who they are?

You don’t have a platform. You have an open invitation to abuse, leaks, and chaos.

Authentication isn’t a bolt-on. It’s a first principle.

And in Phoenix, you have the tools to get it right — if you start with clarity.


Phoenix Doesn’t Prescribe — It Empowers

Phoenix doesn’t force cookies, JWTs, sessions, or OAuth.

Instead, it gives you full control over the request lifecycle.

That flexibility is power — and responsibility.

For APIs, Sessions Don’t Belong

You’re not rendering views. You’re serving:

  • Mobile apps
  • Frontend SPAs
  • Embedded clients
  • Third-party tools

These speak one language:

HTTP + Bearer token in the Authorization header


Step 1: Plug Authentication at the Router

Start in your router. Define an authenticated pipeline:

pipeline :api_auth do
  plug :accepts, ["json"]
  plug MyAppWeb.Plugs.Authenticate
end

scope "/api/v1", MyAppWeb do
  pipe_through :api_auth
  get "/me", UserController, :show
end
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Authenticate Plug

This plug extracts and validates the token:

defmodule MyAppWeb.Plugs.Authenticate do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         {:ok, user} <- MyApp.Auth.verify_token(token) do
      assign(conn, :current_user, user)
    else
      _ -> conn |> send_resp(401, "Unauthorized") |> halt()
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
  • JWT? Decode and verify claims.
  • Opaque token? Look it up hashed in the DB.

Once verified, assign to conn.assigns.current_user — and you're done.


Step 3: Keep Controllers Clean

Because the plug handles auth, your controllers can stay focused:

def show(conn, _params) do
  user = conn.assigns.current_user
  render(conn, "user.json", user: user)
end
Enter fullscreen mode Exit fullscreen mode

No token logic here. Just business logic.


JWT vs. Database Tokens

JWTs

✅ Stateless

✅ Fast to verify

⚠️ Can’t revoke easily

⚠️ Can’t track usage

DB Tokens

✅ Revocable

✅ Traceable

✅ Rotatable

⚠️ Requires storage and lookup

Pick what fits. Control vs. convenience.


Scope & Role-Based Access

Token validity is just the start.

Ask: What does this token authorize?

  • Admins ≠ Regular users
  • 3rd-party clients ≠ Internal services
  • Mobile apps ≠ Browsers

Centralize access logic:

  • Define roles and scopes
  • Create helpers:
  def require_admin(conn, _opts) do
    if conn.assigns.current_user.role != "admin" do
      conn |> send_resp(403, "Forbidden") |> halt()
    else
      conn
    end
  end
Enter fullscreen mode Exit fullscreen mode
  • Use macros or plugs: plug :require_admin when action in [:delete, :update]

Observability: Audit Everything

If you authenticate requests, log them.

Logger.metadata(user_id: user.id, scope: "read:reports")
Logger.info("API request to /api/v1/reports")
Enter fullscreen mode Exit fullscreen mode

Log:

  • user_id
  • request path
  • origin IP
  • scopes used

🔍 It’s how you trace abuse, debug performance issues, and enable audits.


Rate Limiting: Identity-Aware Throttling

Once you know who’s calling — throttle them.

  • Use naive in-memory limiters
  • Or plug in Redis buckets
plug MyAppWeb.Plugs.RateLimiter, scope: :user
Enter fullscreen mode Exit fullscreen mode

Different tokens, different limits:

  • 3rd-party apps? Tight leash
  • Internal clients? Looser limits
  • Admin tokens? Unlimited

Give Clear Errors

Bad tokens happen. Expired, revoked, malformed.

Don’t just return 401 Unauthorized.

Return structured errors:

{
  "error": "token_expired",
  "message": "Your token has expired. Please log in again."
}
Enter fullscreen mode Exit fullscreen mode

Your clients deserve clarity — especially if they’re not under your control.


OAuth? You Can Build It

Phoenix doesn’t ship OAuth — but Ueberauth does.

You can:

  • Issue access/refresh tokens
  • Validate client credentials
  • Manage scopes and grant types

And plug it all right into your Phoenix pipelines.

Your API becomes an auth provider — not just a consumer.


Authentication Isn’t Just About Access. It’s About Confidence.

When you deploy to production, you want to know:

✅ Every request is authenticated

✅ Every token is scoped

✅ Every action is authorized

✅ Every event is logged

✅ Every risk is throttled

✅ Every error is clear

That’s not just security — that’s professionalism.


Build APIs Like a Pro

Your API is the foundation.

Frontends change. Devices evolve. But APIs endure.

It’s not just about fast endpoints.

It’s about building trust — with users, partners, and your future team.


Want the Full Picture?

If you're serious about building production-grade apps with Phoenix LiveView, grab the guide:

Phoenix LiveView: The Pro's Guide to Scalable Interfaces and UI Patterns

  • 20 pages of expert insights
  • Real-world patterns
  • Advanced LiveView techniques

Whether you're designing from scratch or scaling what you’ve built, this guide will make you faster, smarter, and more confident with Phoenix.

Top comments (0)