Hacker News new | ask | show | jobs
by FinalDestiny 1043 days ago
As a new Phoenix convert I agree with a lot of these points from my experience too. The functional components make more sense in the majority of cases (and I think the docs point this out) and having the duplicate auth logic with on_mount feels less than ideal.

Hopefully the team will see this and make it even better!

1 comments

I find a lot of engineers I work with don’t understand on_mount. You shouldn’t have duplicate logic in on_mount—if you do, that code should be in a module. You only have the logic once, but you may call the function twice (though you don’t have to).

All your auth logic (and any other bits you need to set while starting up request state) should be a function in a module somewhere that returns expected values. Then, you just care to set these values to Conn and Socket assigns appropriately, keeping in mind that Conns and Sockets aren’t the same—when you have one, you don’t have the other. Thus, on_mount to the rescue:

1. Use Plug pipeline(s) to call your module function to compute and add bits to Conn.assigns (and you can add those values to the session to avoid computing again, if you’d like).

2. Use on_mount to assign_new:

2a. assign_new pulls from Conn.assigns by key for the dead render while you have a Conn.

2b. The fallback function gets that value again when you no longer have a Conn (recompute or pull from session if already there).

Your actual logic should only exist in one place. And there are easy strategies you can use to avoid expensive computations if necessary.

[Edit]: Not suggesting you don’t understand it, just to be clear. I’ve just experienced a lot of coworkers fighting against, abusing, and misunderstanding Conns, Sockets, and assigns.

Exactly.

Since mix phx.gen.auth handles the authN side, it’s really just authZ that will be idiosyncratic to your application logic.

In every authorization system I’ve written in a Phoenix app (which 8 now), I’ve taken this approach above.

Make a pipeline of rules that take a conn or socket and authorize or not based on what’s in the assigns. Better yet, at the very top of the pipleline, wrap the conn or socket with an auth struct that holds metadata like authorization status and any redirection to be done. Unwrap that struct into the conn or the socket at the end of the pipeline, where the status is resolved.

This way, every request can be default unauthorized and you can apply complex, composable rules for granting permission to take various actions on resources.

In other words phx.gen.auth generates a lot of code that may or may not in part suits your authentification needs and Phoenix gives you nothing for Authorization ;)

(I am a bit bitter after implementing authentication with Firebase and authorization in a Phoenix app? Maybe ahah).

This a bit too negative of an interpretation.

In 100% of the apps where I've used mix phx.gen.auth, the code it's generated has been suitable. In some cases, I've used it in conjunction with a library like Ueberauth for social logins, but it's been strictly superior to older workflows using 3rd party services or frameworks that take over the whole user table.

Reaching for something like Firebase or especially Auth0 has added effort in the long run in each project where I've inherited that decision. The typical end-state seems to be a soup of logic split between the 3rd party provider and inside the application. It's more difficult to reason about and more expensive to audit.

Nothing is going to do your authZ for you, unless it was made with your business logic in mind. Different apps are going to have radically different needs and there isn't a single best solution for all of them.

> This a bit too negative of an interpretation.

Yes :)

> 3rd party services or frameworks that take over the whole user table.

I've never used a Framework when the auth library can't just use your user schema/entity.

I will never agree that using a generator and committing files I didn't write is better DX than using a library and its documentation (which will be easily updatable). It's also kind of limited in scope. Authentication is more than that. Like, JWTs for example.

To each their own.

> Nothing is going to do your authZ for you

The business logic no, the rest yes.

How do I limit a controller or an action for admin users in Symfony ? #[IsGranted('ROLE_ADMIN')], or for a specific user? #[IsGranted('edit', 'post')], done. And the auth/auth are available anywhere in the framework.

How do I do that in Phoenix ? Pull Bodyguard, write a bunch of "with", plugs, or scope with additional code I have to write and maintain.

> How do I do that in Phoenix ?

Assuming you have a `current_user` stored in assigns, I implement basic role access as:

    def edit(conn, param) when conn.assigns.current_user.role == :admin do
      ...
    end
If that's too long:

    defguard is_granted(conn, role) when conn.assigns.current_user.role == role

    def edit(conn, param) when is_granted(conn, :admin) do
      ...
    end
It also works on LiveViews with guards on handle_event.

---

The other way of doing authorization, which IIRC is similar to voters in Symfony, I typically implement with protocols:

    defprotocol Authz do
      def can?(resource, action, user)
    end
Then in my Post schema:

    defmodule MyApp.Blog.Post do
      schema "posts" do
        # ...
      end

      defimpl Authz do
        def can?(post, :view, user) do
          post.public or can?(post, :edit, user)
        end

        def can?(post, :edit, user) do
          post.owner_id == user.id
        end
      end
    end
You may encapsulate in a controller helper like this, although you cannot use it in guards:

    def can?(conn, resource, action) do
      Authz.can?(resource, action, conn.assigns.current_user)
    end
Now you can call it to check against any resource whatsoever. If you don't implement it for a resource or for an action, it will crash as expected.

I find it requires less boilerplate than the Voter approach in Symfony (but it has been quite some time since I last checked it). No additional abstractions either. The only downside is that it doesn't work annotation/@decorator style (but if you really want it, it should be doable).

---

However, my favorite way of doing authz is by scoping the queries. Typically all of my context functions receive either the org, the user, or a "session" data structure with both which I use as the starting point of my queries. Then I complement with Authz.can? style when that's not enough.

can you provide sample code of one of your auth you have created, I would like to learn how to code the pipelines as your mention

thanks a bunch

Do you mind briefly expanding on your pipeline process?