Broken Access Control in Phoenix with Bearer Tokens
Broken Access Control in Phoenix with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when authorization checks are missing, incomplete, or bypassed, allowing a user to access or modify resources they should not. In the Phoenix web framework, this commonly maps to the OWASP API Top 10 category and can be triggered when APIs rely solely on Bearer Tokens for authentication but do not enforce proper authorization per request. A token may identify a user or a scope, yet if routes or controllers do not validate that the authenticated subject is permitted to perform the action on the specific resource, the API is vulnerable.
Consider a typical Phoenix pipeline that authenticates with a Bearer Token but skips authorization checks for certain endpoints:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.AuthenticateApiToken # validates Bearer Token, assigns current_user
end
scope "/api", MyAppWeb do
pipe_through :api
get "/documents/:id", DocumentController, :show # ← missing authorization check
patch "/documents/:id", DocumentController, :update
end
end
In this setup, the AuthenticateApiToken plug validates the Bearer Token and assigns a current_user in the connection assigns. However, if DocumentController does not verify that current_user has permission to view or modify the given :id, an authenticated user can tamper with the :id parameter and access or edit other users’ documents. This is a classic BOLA/IDOR pattern enabled by weak authorization around Bearer Token authentication.
Broken Access Control with Bearer Tokens can also arise when token scopes or roles are issued but not validated on each request. For example, an API might issue a token with a read:documents scope but the Phoenix endpoint performs no scope checking:
defmodule MyAppWeb.Plugs.AuthorizeDocumentScope do
import Plug.Conn
def init(default), do: default
def call(conn, _opts) do
case conn.assigns[:current_user] do
%{scopes: scopes} when "read:documents" in scopes -> conn
_ -> Plug.Conn.send_resp(conn, 403, "{\"error\": \"insufficient_scope\"}") |> halt()
end
end
end
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.AuthenticateApiToken
plug MyAppWeb.Plugs.AuthorizeDocumentScope # ensures token has required scope
end
scope "/api", MyAppWeb do
pipe_through :api
get "/documents/:id", DocumentController, :show
end
end
Even with scope validation, you must also enforce per-instance authorization (e.g., the document belongs to the user or the user’s team). Relying only on token-level attributes without checking the resource owner enables horizontal privilege escalation across records. This is why middleware authentication (Bearer Tokens) must be paired with explicit, request-level authorization checks in Phoenix controllers or via domain policies, mapping directly to the BOLA/IDOR checks performed by middleBrick’s scans.
Bearer Tokens-Specific Remediation in Phoenix — concrete code fixes
Remediation focuses on ensuring that every authenticated request is also authorized for the specific resource and required scope. Below are concrete, idiomatic Phoenix patterns that address Broken Access Control when using Bearer Tokens.
1. Add per-resource ownership check in the controller
After authenticating the token and assigning current_user, verify that the requested resource belongs to the user or an authorized role/team before proceeding:
defmodule MyAppWeb.DocumentController do
use MyAppWeb, :controller
plug :authorize_document when action in [:show, :update]
def show(conn, %{"id" => id}) do
document = MyApp.get_document!(id)
# Ensure the document belongs to the authenticated user or their org
if MyApp.authorized?(document, conn.assigns.current_user) do
render(conn, "show.json", document: document)
else
send_resp(conn, 403, "{\"error\": \"forbidden\"}")
end
end
defp authorize_document(conn, _opts) do
with %{"id" => id} <- conn.params,
document <- MyApp.get_document(id),
user when not is_nil(user) <- conn.assigns[:current_user],
true <- MyApp.authorized?(document, user) do
attach_document(conn, document)
else
_ -> send_resp(conn, 403, "{\"error\": \"forbidden\"}") |> halt()
end
end
end
2. Validate token scopes and enforce them via a plug
Define a plug that inspects the token’s scopes (embedded in the token claims or fetched from a cache) and rejects requests missing required permissions:
defmodule MyAppWeb.Plugs.RequireScope do
import Plug.Conn
def init(required_scope), do: required_scope
def call(conn, required_scope) do
user = conn.assigns[:current_user]
if Enum.member?(Map.get(user, :scopes, []), required_scope) do
conn
else
send_resp(conn, 403, "{\"error\": \"insufficient_scope\"}") |> halt()
end
end
end
# Usage in router
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.AuthenticateApiToken
plug MyAppWeb.Plugs.RequireScope, "write:documents"
end
3. Use domain policies for centralized authorization logic
Keep authorization decisions in a dedicated module so controllers remain thin and checks are consistent across endpoints:
defmodule MyApp.Policies.Document do
def can_view?(%Document{user_id: owner_id}, %User{id: user_id}), do: owner_id == user_id
def can_update?(%Document{owner_id: owner_id}, %User{id: user_id}), do: owner_id == user_id
# Add team-based or role-based rules as needed
end
# In controller or plug
if MyApp.Policies.Document.can_view?(document, conn.assigns.current_user) do
# proceed
end
By combining Bearer Token authentication in Phoenix with explicit, per-request authorization checks—covering ownership, scopes, and domain policies—you mitigate the risk of Broken Access Control and reduce the findings that tools like middleBrick would report under BOLA/IDOR and related checks.