Hopefully the team will see this and make it even better!
Hopefully the team will see this and make it even better!
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.
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.
(I am a bit bitter after implementing authentication with Firebase and authorization in a Phoenix app? Maybe ahah).
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.
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.
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.
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 ;)
Follow up question: do you think the above would be helpful as a short-guide on Phoenix?
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.
thanks a bunch