phoenix-authorization-patterns
Skill4 starsChanged 4 months agoMITView on GitHub
Handles all authorization and access control work in Phoenix and LiveView applications. Covers server-side authorization, owner-only patterns, scoped queries, policy modules, controller authorization, and authorization testing. Use when writing permission checks, policy modules, or role-based access controls, or when addressing authorization, access control, permission, policy, role, owner, scoped query, or IDOR concerns.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 33
Follow these steps in order when adding authorization to any new resource:
- Distinctive: line 42
Every context function that returns or mutates a resource is scoped to the current user ✓
- Very specific: line 44
Every mutating handle_event has a corresponding unauthorized-path test ✓
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Phoenix Authorization Patterns
RULES — Follow these with no exceptions
1. Always authorize on the server in event handlers — never rely on UI-only checks
2. Verify resource ownership by comparing current_scope.user.id against the resource's user_id — never trust client-sent user IDs
3. Use policy modules for complex authorization — don't inline permission checks in LiveViews or controllers
4. Add data-confirm attribute for destructive UI actions — client-side confirmation before server round-trip
5. Test both authorized and unauthorized paths — every handle_event that mutates data needs an authz test
6. Scope queries to the current user in contexts — where(user_id: ^user_id) prevents IDOR vulnerabilities
Authorization Workflow for a New Resource
Follow these steps in order when adding authorization to any new resource:
- Add scoped queries in the context — ensure all queries filter by
user_idso unauthorized data is never returned - Define policy module rules — add clauses for every action (
view,edit,delete, etc.) before wiring up any LiveView - Add server-side checks in LiveView event handlers — call
Policy.authorize/3or comparecurrent_scope.user.idagainst the resource'suser_idin everyhandle_eventthat mutates data - Write unauthorized-path tests first — confirm that a non-owner receives an error and that the mutation does not occur
- Only then add UI controls — hide or disable buttons for unauthorized users after server-side checks are verified
Validation checkpoints:
- Every context function that returns or mutates a resource is scoped to the current user ✓
- Policy module has a catch-all clause returning
{:error, :unauthorized}✓ - Every mutating
handle_eventhas a corresponding unauthorized-path test ✓
Server-Side Authorization in LiveViews
defmodule MyAppWeb.PostLive.Show do
use MyAppWeb, :live_view
@impl true
def handle_event("delete", _params, socket) do
post = socket.assigns.post
if socket.assigns.current_scope.user.id == post.user_id do
{:ok, _} = Blog.delete_post(post)
{:noreply, push_navigate(socket, to: ~p"/posts")}
else
{:noreply, put_flash(socket, :error, "Not authorized")}
end
end
endScoped Queries in Contexts
defmodule MyApp.Blog do
import Ecto.Query
# Scoped — only returns posts owned by this user
def list_user_posts(%Scope{user: user}) do
Post
|> where(user_id: ^user.id)
|> order_by(desc: :inserted_at)
|> Repo.all()
end
# Scoped get — returns nil if not owned by user
def get_user_post(%Scope{user: user}, id) do
Post
|> where(user_id: ^user.id)
|> Repo.get(id)
end
# Scoped update — only updates if owned
def update_user_post(%Scope{user: user}, %Post{} = post, attrs) do
if post.user_id == user.id do
post |> Post.changeset(attrs) |> Repo.update()
else
{:error, :unauthorized}
end
end
endPolicy Modules
For complex permissions (roles, teams, org-level access):
defmodule MyApp.Policy do
alias MyApp.Accounts.User
alias MyApp.Blog.Post
def authorize(%User{role: :admin}, _action, _resource), do: :ok
def authorize(%User{id: user_id}, :edit, %Post{user_id: user_id}), do: :ok
def authorize(%User{id: user_id}, :delete, %Post{user_id: user_id}), do: :ok
def authorize(%User{}, :view, %Post{published: true}), do: :ok
def authorize(_user, _action, _resource), do: {:error, :unauthorized}
end
# Usage in LiveView
case Policy.authorize(user, :delete, post) do
:ok -> {:ok, _} = Blog.delete_post(post)
{:error, :unauthorized} -> put_flash(socket, :error, "Not authorized")
endTesting Authorization
describe "authorization" do
test "owner can delete their post", %{conn: conn} do
user = user_fixture()
post = post_fixture(user_id: user.id)
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/posts/#{post}")
lv |> element("button", "Delete") |> render_click()
assert_redirect(lv, ~p"/posts")
end
test "non-owner cannot delete post", %{conn: conn} do
owner = user_fixture()
other_user = user_fixture()
post = post_fixture(user_id: owner.id)
conn = log_in_user(conn, other_user)
{:ok, lv, _html} = live(conn, ~p"/posts/#{post}")
refute render(lv) =~ "Delete"
assert render_click(lv, "delete") =~ "Not authorized"
end
endRelated Skills
| Skill | Purpose |
| phoenix-liveview-auth | Authentication (who you are) |
| phoenix-scopes | Phoenix 1.8+ Scope-based auth |
| testing-essentials | Testing patterns |
| security-essentials | Broader security best practices |
Common Pitfalls
| ❌ Don't | ✅ Do |
| Rely on hidden or disabled UI buttons for access control | Authorize on the server in every handle_event |
Trust a user_id sent from the client | Compare current_scope.user.id against the resource's user_id |
| Query a resource without a user filter | Scope queries with where(user_id: ^user.id) to prevent IDOR |
| Inline complex permission logic in LiveViews/controllers | Extract rules into a Policy module |
| Omit a catch-all authorize clause | End Policy with def authorize(_, _, _), do: {:error, :unauthorized} |
| Test only the happy path | Test both authorized and unauthorized paths |
Integration
| Predecessor | This Skill | Successor |
| phoenix-liveview-essentials | phoenix-authorization-patterns | security-essentials |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

