Hacker News new | ask | show | jobs
by AlchemistCamp 1042 days ago
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.

1 comments

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

That's sensible ways of doing things obviously. The can? approach is very similar to the Canada lib IIRC. I'll probably try some of those.

But the best non-business code I like is the one I don't have to code, commit and maintain.

You don't need voters for the example I wrote earlier though, in fact I don't remember the last time I had to write one.

But I think it's more a functional vs OO discussion and we know how those goes ;)

I can see how #[IsGranted(role)] would work without voters but how would #[IsGranted(post, view)] work without voters? Would it dispatch to the post object? If so, then it is pretty much the same as protocols, yeah.

Follow up question: do you think the above would be helpful as a short-guide on Phoenix?

Yes you're probably right about the voter, I've probably mixed it up with the automatic parameter conversion for routing.

I've done too much Phoenix lately I forgot the rest ;)

About the guide I don't know, maybe a blog post yeah, but there is libs that do all that, maybe there's even too many of them. They are fine but lack the cohesiveness you get with something integrated in a framework.