---
title: "Add JWT Authentication to Your Phoenix API"
description: "Secure your Phoenix API using JWT authentication with JWKS."
canonicalUrl: "https://zuplo.com/use-cases/api-key-auth/elixir/phoenix/jwt-backend"
framework: "Phoenix"
language: "Elixir"
authStrategy: "JWT with JWKS"
pageType: use-case
---

# Add JWT Authentication to Your Phoenix API

Secure your Phoenix API using JWT authentication with JWKS.

## How Zuplo Handles It

Let Zuplo issue short-lived JWTs signed with a JWKS your Phoenix backend can verify — no long-lived API keys touch your origin.

## Phoenix Backend Code

```elixir
# lib/my_app_web/plugs/jwt_auth.ex
defmodule MyAppWeb.Plugs.JwtAuth do
  import Plug.Conn
  require Logger

  @issuer "https://my-api-a32f34.zuplo.api/__zuplo/issuer"
  @jwks_uri "#{@issuer}/.well-known/jwks.json"

  def init(opts), do: opts

  def call(conn, _opts) do
    with {:ok, token} <- extract_token(conn),
         {:ok, claims} <- verify_token(token) do
      assign(conn, :current_user, claims)
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(401, Jason.encode!(%{error: "Unauthorized", details: reason}))
        |> halt()
    end
  end

  defp extract_token(conn) do
    case get_req_header(conn, "authorization") do
      ["Bearer " <> token] -> {:ok, token}
      _ -> {:error, "No token provided"}
    end
  end

  defp verify_token(token) do
    with {:ok, jwks} <- fetch_jwks(),
         {:ok, claims} <- decode_and_verify(token, jwks) do
      if claims["iss"] == @issuer do
        {:ok, claims}
      else
        {:error, "Invalid issuer"}
      end
    end
  end

  defp fetch_jwks do
    # Use ETS or Cachex for production caching
    case :ets.lookup(:jwks_cache, :jwks) do
      [{:jwks, jwks, timestamp}] when timestamp > :os.system_time(:second) - 600 ->
        {:ok, jwks}
      _ ->
        case HTTPoison.get(@jwks_uri) do
          {:ok, %{status_code: 200, body: body}} ->
            jwks = Jason.decode!(body)
            :ets.insert(:jwks_cache, {:jwks, jwks, :os.system_time(:second)})
            {:ok, jwks}
          _ ->
            {:error, "Failed to fetch JWKS"}
        end
    end
  end

  defp decode_and_verify(token, jwks) do
    case JOSE.JWT.verify_strict(jwks, ["RS256"], token) do
      {true, %{fields: claims}, _} -> {:ok, claims}
      _ -> {:error, "Invalid token"}
    end
  end
end

# lib/my_app_web/controllers/protected_controller.ex
defmodule MyAppWeb.ProtectedController do
  use MyAppWeb, :controller

  def show(conn, _params) do
    json(conn, %{
      message: "Access granted",
      user: conn.assigns[:current_user]
    })
  end
end

# lib/my_app_web/router.ex
# pipeline :jwt_auth do
#   plug MyAppWeb.Plugs.JwtAuth
# end
#
# scope "/api", MyAppWeb do
#   pipe_through [:api, :jwt_auth]
#   get "/protected", ProtectedController, :show
# end
```

## Example Request

```bash
curl -X GET \
  'https://your-api.zuplo.dev/your-route' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

## Learn More

- [API Key Authentication on Zuplo](https://zuplo.com/docs/policies/api-key-auth-inbound)
- [JWT Authentication on Zuplo](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound)
- [All use cases](https://zuplo.com/use-cases)
