angelmz

angelmz

Phx.gen.auth and role based authentication

Is phx.gen.auth good enough for role based authentication?
Let’s say for example one was building an e-commerce store.
There would standard users, admins, and sellers.
Could one do:

mix phx.gen.auth Accounts User users
mix phx.gen.auth Accounts Admin admins
mix phx.gen.auth Accounts Sellers sellers

I know that would create a lot of duplicate code, but if you got in there and specified which functions were fore each role, would this work? Also, could this be adaptable to phx.gen.auth? I couldn’t find anything regarding roles in the documentation.

Second alternative: Could one have and admins table that inherits the users table? I’m fairly new to Ecto and Postgres and I don’t know if that works here.

Thank you for your time

Marked As Solved

derpycoder

derpycoder

This is the best resource to understand and implement Authorization flow:

https://www.leanpanda.com/blog/authentication-and-authorisation-in-phoenix-liveview/#step-4-create-a-way-to-logout-a-user-and-close-all-his-her-sessions


I took it to next level with FunWithFlags feature toggles!!

Here are the reason:

  • Role based: falls short, as it suffers from Role Explosion.

  • Claim/Group Based: falls short, as anyone can claim to be something they are not! (e.g. Underage user claiming to be able to drink!!)

  • Policy Based: falls short, because once you define a policy that someone must be above 18 yrs old to claim to be able to drink, you won’t be able to change that at run time!!

  • Permission Based, it’s a combination of all of the above, so I have all the flexibility I could think of, and I can grant or deny access at runtime!! (It falls short because the decision is boolean, i.e. True or False, but world isn’t black or white)

  • Scope Based: Later I will add scopes, so I can limit access even further on top of Permissions. (For instance, an Admin shouldn’t be able to view Private user posts, or One Tenant can’t change data of another Tenant, etc)

For example:

# ==============================================================================
# Scope: Used to fine tune the output from the Repo
# ==============================================================================

# Signed in users can delete their own photos within same org
defp scope_photos(query, %User{role: :user, id: id}, _params) do
   query
   |> where(author_id: ^id)
   |> or_where(state: :published) # Just an example
end

Or maybe I will create a web page, so I can control the scope dynamically.

See how to dynamically write business logic on web; opus, exop, espresso:

For more info on RBAC vs CBAC, see:


Here’s how it looks right now:

lib/derpy_coder/photos.ex

defmodule DerpyCoder.Photos do
  @moduledoc """
  The Photos context.
  """

  defdelegate can?(user, action, entity), to: DerpyCoder.Photos.Policy

  # ==============================================================================
  # Verify that the user is authorized.
  # ==============================================================================
  def verify_authorization({:cont, socket}, action, entity) do
    user = socket.assigns.current_user

    if can?(user, action, entity) do
      {:cont, socket}
    else
      {:halt, socket |> LiveHelpers.kick_unauthorized_user_out()}
    end
  end

  def verify_authorization({:halt, _} = arg, _, _), do: arg

  def verify_authorization({:cont, socket}, entity) do
    user = socket.assigns.current_user
    action = socket.assigns.live_action

    if can?(user, action, entity) do
      {:cont, socket}
    else
      {:halt, socket |> LiveHelpers.kick_unauthorized_user_out()}
    end
  end

  def verify_authorization({:halt, _} = arg, _), do: arg

  def verify_authorization({:cont, socket}) do
    user = socket.assigns.current_user
    action = socket.assigns.live_action

    if can?(user, action, Photo) do
      {:cont, socket}
    else
      {:halt, socket |> LiveHelpers.kick_unauthorized_user_out()}
    end
  end

  def verify_authorization({:halt, _} = arg), do: arg
  ...
end

lib/derpy_coder/photos/policy.ex

defmodule DerpyCoder.Photos.Policy do
  @moduledoc """
  Policy: Used to authorize user access
  """
  alias DerpyCoder.Accounts.User
  alias DerpyCoder.Photos.Photo

  @type entity :: struct()
  @type action :: :new | :index | :edit | :show | :delete

  @spec can?(User, action(), entity()) :: boolean()
  def can?(user, action, entity)

  # ==============================================================================
  # Super Admin
  # ==============================================================================
  def can?(%User{id: id, role: :super_admin}, _, _), do: DerpyCoder.Accounts.is_super_admin?(id)

  # ==============================================================================
  # Admin
  # ==============================================================================
  def can?(%User{role: :admin} = user, :new, _),
    do: FunWithFlags.enabled?(:new_photos, for: user)

  def can?(%User{role: :admin} = user, :edit, _),
    do: FunWithFlags.enabled?(:edit_photos, for: user)

  def can?(%User{role: :admin} = user, _, Photo), do: FunWithFlags.Group.in?(user, :photography)
  def can?(%User{role: :admin}, _, _), do: true

  # ==============================================================================
  # User
  # ==============================================================================
  def can?(%User{} = user, :new, Photo), do: FunWithFlags.enabled?(:new_photos, for: user)

  def can?(%User{id: id} = user, :edit, %Photo{user_id: id}),
    do: FunWithFlags.enabled?(:edit_photos, for: user)

  def can?(%User{id: id} = user, :delete, %Photo{user_id: id}),
    do: FunWithFlags.enabled?(:delete_photos, for: user)

  def can?(_, _, _), do: false
end

Usage in HEEX:

<%= if Photos.can?(@current_user, :new, Photo) do %>
    <%= live_patch("New Photo", to: Routes.photo_index_path(@socket, :new)) %>
<% end %>

<%= if Photos.can?(@current_user, :edit, photo) do %>
     <%= live_patch("Edit", to: Routes.photo_index_path(@socket, :edit, photo)) %>
<% end %><%= if Photos.can?(@current_user, :delete, photo) do %>

<%= if Photos.can?(@current_user, :delete, photo) do %>
    <%= link("Delete",
        to: "#",
        phx_click: "delete",
        phx_value_id: photo.id,
        data: [confirm: "Are you sure?"]
    ) %>
<% end %>

In EX Side:

defp apply_action(socket, :new, _params) do
    {:cont, socket}
    |> verify_user()
    |> verify_email()
    |> Photos.verify_authorization()
    |> case do
      {:cont, socket} ->
        socket
        |> assign(:photo, %Photo{})

      {:halt, socket} ->
        socket
    end
end

defp apply_action(socket, :edit, params) do
    photo = photo_from_params(params)

    {:cont, socket}
    |> verify_user()
    |> Photos.verify_authorization(photo)
    |> case do
      {:cont, socket} ->
        socket
        |> assign(:photo, photo)

      {:halt, socket} ->
        socket
    end
end

def handle_event("delete", params, socket) do
    photo = photo_from_params(params)

    {:cont, socket}
    |> verify_user()
    |> Photos.verify_authorization(photo, :delete)
    |> case do
      {:cont, socket} ->
        {:ok, _} = Photos.delete_photo(photo)

        {:noreply,
         socket
         |> assign(:photos, list_photos())}

      {:halt, socket} ->
        {:noreply, socket}
    end
end

Similary for other resources:

<%= if Accounts.can?(@current_user, :view, DerpyCoderWeb.UserDashboardLive) do %>
    <%= live_redirect("User Dashboard", to: Routes.user_dashboard_path(@socket, :index)) %>
<% end %>

Here’s how to grant or deny Permissions:

defmodule DerpyCoderWeb.Permissions do
  @moduledoc """
  Used to initialize the default permissions for each group or resource.
  Powered by FunWithFlags.
  """

  @doc """
  Used to grant permissions to groups or resources.
  """
  def grant() do
    # Since Indexing & Viewing is public by default, adding these permissions don't make sense!
    # FunWithFlags.enable(:index_photos, for_group: :photography)
    # FunWithFlags.enable(:show_photos, for_group: :photography)
    FunWithFlags.enable(:new_photos, for_group: :photography)
    FunWithFlags.enable(:edit_photos, for_group: :photography)
    FunWithFlags.enable(:delete_photos, for_group: :photography)
  end

  @doc """
  Used to redact permissions for groups & resources.
  """
  def deny() do
    # If Admin, is to be treaded like normal user, then below flags can be removed.
    # And admin's new & edit power can be restricted in scope.
    FunWithFlags.disable(:new_photos, for_group: :admin)
    FunWithFlags.disable(:edit_photos, for_group: :admin)
  end
end

Seeds.exs

# ==============================================================================
# Creating Default Users
# ==============================================================================
{:ok, admin} =
  Accounts.seed_admin(%{
    email: "admin@derpycoder.com",
    password: "wubalubadubdub",
    password_confirmation: "wubalubadubdub",
    groups: ~w(admin photography)a
  })

{:ok, abhijit} =
  Accounts.seed_user(%{
    email: "abhijit@derpycoder.com",
    password: "wubalubadubdub",
    password_confirmation: "wubalubadubdub",
    groups: ~w(photography)a
  })

Here’s how I enable or disable access in LiveBook.


derp = Accounts.get_user_by_email("derp@derpycoder.com")

# Disable Access
FunWithFlags.disable(:new_photos, for_actor: derp)
FunWithFlags.disable(:delete_photos, for_actor: derp)

# Re enable Access
FunWithFlags.enable(:new_photos, for_actor: derp)
FunWithFlags.enable(:delete_photos, for_actor: derp)

Later I will have a web view for toggling feature, permission and any other types of flags. (I wanted unified UI instead of using UI that comes with Fun With Flags!!)


P.S. It made the testing difficult, will try to figure out testing after Phoenix 1.7 is out.

Also Liked

tomkonidas

tomkonidas

I would do it once with mix phx.gen.auth Accounts User users and then update the migration to have a role enum field (could be [:customer, :admin, :seller]). That is the good thing about get.auth, it lets you modify the code after the fact

al2o3cr

al2o3cr

It’s hard to say whether this is the “right” approach - it’s an approach, but whether it’s right depends on what the application does with users.

For instance, if all three kinds of users do the same sorts of things - or even more, if they all end up attached to the same records - then keeping them in three tables will be a headache.

On the flip side, if they do very different things this approach could be valuable since it’s possible to enforce things like “this can only point to a seller”. There may even be security benefits in some situations, since a User cannot become an Admin by overwriting columns in the database.

Another alternative approach: make a model that manages just the email + password part (call it something like Credential) with phx.gen.auth, then have a credential_id on Admin / User / Seller etc. This could help reduce the duplication of things like password reset machinery etc (IF that machinery is supposed to be the same!)

tadasajon

tadasajon

I’ve spent some time building a Phoenix app with auth and if I had to do it all over again I would use Supabase.com because they not only give you auth, but they also give you a very slick Postgres layer. You can still use Elixir/Phoenix to connect to it. Using Supabase for data+auth and connecting to it with Phoenix seems to me to be the most flexible approach all around. Auth is such a pain-in-the-neck when you don’t basically outsource it.

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement