agentleFS
igmarinigmarin/elixir-phoenix-skills/skills/phoenix-authorization-patterns/SKILL.md

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.

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
  1. Distinctive:

    Follow these steps in order when adding authorization to any new resource:

    line 33
  2. Distinctive:

    Every context function that returns or mutates a resource is scoped to the current user ✓

    line 42
  3. Very specific:

    Every mutating handle_event has a corresponding unauthorized-path test ✓

    line 44

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. Add scoped queries in the context — ensure all queries filter by user_id so unauthorized data is never returned
  2. Define policy module rules — add clauses for every action (view, edit, delete, etc.) before wiring up any LiveView
  3. Add server-side checks in LiveView event handlers — call Policy.authorize/3 or compare current_scope.user.id against the resource's user_id in every handle_event that mutates data
  4. Write unauthorized-path tests first — confirm that a non-owner receives an error and that the mutation does not occur
  5. 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_event has 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
end

Scoped 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
end

Policy 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")
end

Testing 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
end

Related Skills

SkillPurpose
phoenix-liveview-authAuthentication (who you are)
phoenix-scopesPhoenix 1.8+ Scope-based auth
testing-essentialsTesting patterns
security-essentialsBroader security best practices

Common Pitfalls

❌ Don't✅ Do
Rely on hidden or disabled UI buttons for access controlAuthorize on the server in every handle_event
Trust a user_id sent from the clientCompare current_scope.user.id against the resource's user_id
Query a resource without a user filterScope queries with where(user_id: ^user.id) to prevent IDOR
Inline complex permission logic in LiveViews/controllersExtract rules into a Policy module
Omit a catch-all authorize clauseEnd Policy with def authorize(_, _, _), do: {:error, :unauthorized}
Test only the happy pathTest both authorized and unauthorized paths

Integration

PredecessorThis SkillSuccessor
phoenix-liveview-essentialsphoenix-authorization-patternssecurity-essentials

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.