Broken Authentication in Phoenix with Bearer Tokens
Broken Authentication in Phoenix with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Authentication in a Phoenix API that uses Bearer Tokens typically arises when token issuance, storage, transmission, or validation is implemented insecurely. Even when the protocol is Bearer-based, implementation choices in Phoenix/Elixir can inadvertently expose tokens or accept malformed ones.
One common pattern is using the Plug.Parsers pipeline to read JSON bodies, but then relying on a custom header like authorization parsed with String.slice(req_header, 7, String.length(req_header) - 7) without verifying the token format or ensuring HTTPS. If the endpoint does not enforce HTTPS in production, an attacker on the network can capture the raw Bearer token from an Authorization: Bearer <token> header.
Phoenix applications that use Guardian or similar JWT libraries may incorrectly configure the Phoenix.Token signer or the Guardian.expiration_time, leading to tokens that never expire or use a weak signing algorithm such as none or a static secret. A concrete misconfiguration example is passing an empty or low-entropy secret to Guardian, which makes token forgery feasible. Insecure storage on the client side (e.g., storing tokens in localStorage) combined with a Phoenix endpoint that does not set HttpOnly flags for session cookies (if used) can enable cross-site scripting (XSS) to steal tokens.
Another vector involves IDOR when a Phoenix route like /api/users/:id accepts a Bearer token but uses the token’s embedded subject to fetch user data without confirming that the subject matches the requesting user’s permissions. For example, decoding a JWT to get sub and then calling Accounts.get_user!(sub) without scoping the query by the current connection’s actor can allow enumeration or unauthorized data access. OWASP API Top 10:2023 A07:2023 (Identification and Authentication Failures) maps directly to these patterns, as does the CWE-287 Improper Authentication category.
Runtime scans can detect these issues by checking whether the API rejects requests without a token, accepts tokens with weak algorithms, leaks tokens in logs or error messages, or returns different behaviors for missing versus invalid tokens. An OpenAPI spec that defines securitySchemes as type http with bearer format should be cross-referenced with actual runtime behavior; mismatches between declared security requirements and implementation are a strong indicator of Broken Authentication risk.
To illustrate a typical Bearer token usage in Phoenix, consider the following pipeline and controller. This example shows how tokens are read and verified, but also highlights where missing checks can lead to broken authentication:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.AuthPlug
end
scope "/api", MyAppWeb do
pipe_through :api
get "/profile", ProfileController, :show
end
end
defmodule MyAppWeb.AuthPlug do
import Plug.Conn
alias MyApp.Accounts
def init(opts), do: opts
def call(conn, _opts) do
case get_bearer_token(conn) do
{:ok, token} ->
case Accounts.verify_token(token) do
{:ok, user} -> assign(conn, :current_user, user)
{:error, _} -> send_resp(conn, 401, "Unauthorized")
end
:error -> send_resp(conn, 401, "Missing token")
end
end
defp get_bearer_token(conn) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization") do
{:ok, token}
else
_ -> :error
end
end
end
defmodule MyApp.Accounts do
def verify_token(token) do
# Insecure example: no expiration check, static secret
case Guardian.decode_and_verify(token, "weak_static_secret", ["typ": "JWT"]) do
{:ok, claims} -> {:ok, %{id: claims["sub"]}}
{:error, _} -> {:error, :invalid}
end
end
end
In this example, note the lack of HTTPS enforcement, static secret usage, missing token revocation checks, and no binding between the token subject and the requested resource. These gaps can enable token replay, credential theft, and privilege escalation, which middleBrick scans for as part of its Authentication and BOLA/IDOR checks.
Bearer Tokens-Specific Remediation in Phoenix — concrete code fixes
Remediation for Broken Authentication with Bearer Tokens in Phoenix focuses on secure token handling, strict validation, and proper scoping. The following changes address the weaknesses in the previous example.
First, enforce HTTPS in production to protect the token in transit. Use Plug.SSL to redirect HTTP requests and ensure the authorization header is only inspected over secure connections:
# In your endpoint or router
if Mix.env() == :prod do
plug Plug.SSL, rewrite_on: [:x_forwarded_proto], host: true
end
Second, replace the static secret with a properly rotated key and use a library like Guardian with a configured issuer and algorithm. Configure Guardian in config/runtime.exs:
config :my_app, MyApp.Guardian,
issuer: "my_app",
secret_key: System.fetch_env!("GUARDIAN_SECRET_KEY"),
ttl: {30, :days},
verify_issuer: true
Update the verification function to validate claims, including expiration (exp) and issuer (iss), and scope the user lookup to the token subject:
defmodule MyApp.Accounts do
use Guardian, otp_app: :my_app
def verify_token(token) do
with {:ok, claims} <- decode_and_verify(token, %{"iss" => "my_app"}, verify: [aud: "my_app"]),
"true" <- claims["is_active"] do
user = Accounts.get_user_by_sub!(claims["sub"])
{:ok, user}
else
_ -> {:error, :invalid}
end
end
end
Third, ensure the token is transmitted only via the Authorization header and never logged. In AuthPlug, avoid leaking the full header in logs and normalize token extraction:
defp get_bearer_token(conn) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] when byte_size(token) > 0 and byte_size(token) <= 4096 ->
{:ok, token}
_ ->
:error
end
end
Fourth, bind the token subject to the resource being accessed. In your controller, fetch the user from the token and ensure the requested ID matches:
defmodule MyAppWeb.ProfileController do
use MyAppWeb, :controller
def show(conn, _params) do
current_user = conn.assigns.current_user
# Ensure the profile being requested belongs to the authenticated subject
profile = Accounts.get_profile_for_user!(current_user.id)
render(conn, "show.json", profile: profile)
end
end
Fifth, add token revocation and refresh handling. Store a token version (e.g., a jti claim) in the database and check it during verification:
def verify_token(token) do
with {:ok, claims} <- Guardian.decode_and_verify(token),
true <- TokenRevoked.check_jti_not_revoked(claims["jti"]) do
Accounts.get_user_by_sub!(claims["sub"])
end
end
These steps align with checks performed by middleBrick’s Authentication, BOLA/IDOR, and BFLA/Privilege Escalation scans. The scanner validates that tokens are not accepted over insecure channels, that algorithms are not downgraded, and that subjects are properly bound to resources, reducing the risk of Broken Authentication.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |