We are going to build a very simple user authorization system for phoenix framework which can fit into applications of different size.
Basically authorization is allowing a user
or an entity to do something
if it is allowed
to do that. There are multiple approaches to address this. But this one matches my requirements and it allows me to easily apply this logic between multiple projects. As usual there are libraries to do this. But in my case it was not necessary.
So lets assume we want to authorize "a user who created a post is the only one who can delete the post". Basically, We can simply create a function to do that and call it from the action accessing it.
def authorize_delete_post(user, post) do
if(user.id == post.user_id) do
{:ok}
else
{:unauthorized}
end
end
This is the simplest type of authorization. It will work fine, But if you are reading this it is because you need a better solution.
Authorize module
It is recommended to move all authorization logic to a common place. So, we can create a new module Authorize
and move it there.
defmodule MyApp.Authorize do
alias MyApp.Accounts.User
alias MyApp.Posts.Post
def check(:delete_post, %User{id: user_id}, %Post{user_id: user_id}), do: {:ok}
def check(:delete_post, %User{}, %Post{}), do: {:unauthorized}
end
For now on we can call Authorize.check(:delete_post, user, post)
to authorize the user to delete the post. it will return {:ok}
if user_id
of post is equal to id
of user.
We can do some refactoring here, because there will be need to authorize many more actions. But remember, this part should be done based on your application's requirements.
defmodule MyApp.Authorize
alias MyApp.Accounts.User
alias MyApp.Posts.Post
def check(:delete_post, %User{}=user, %Post{}=post), do: authorize_created_by(user, post)
defp authorize_created_by(%User{id: user_id}, %{user_id: user_id}), do: ok
defp authorize_created_by(%User{}, _), do: unauthorized
defp ok, do: {:ok}
defp unauthorized, do: {:unauthorized}
end
we can latter add
def check(:update_post, %User{}=user, %Post{}=post), do: authorize_created_by(user, post)
to do authorization for updating the post.
That concludes our implementation part. Now we can do some optimizations on the controller end.
sending common error messages on authorization error.
we have a plug in phoenix controller called action_fallback
that allows us to call another plug as a fallback action. Follow the examples from https://hexdocs.pm/phoenix/Phoenix.Controller.html#action_fallback/1 to understand how to do that. I don't want to cover it here since it is already well documented. Also as a bonus you have information on how you should call our authorization module in there. So, in our case the FallbackController will have this plug.
def call(conn, {:unauthorized}) do
# do something and return 403
end
Conclusion
The advantages of following this approach are, It is simple to read and easy to implement. Also it is easy to optimize it further based on your application's business logic.
Discussion (2)
There is confusion at the beginning of the article.
I think you need to replace:
if(user.id == post.id) do
if(user.id == post.user_id) do
Thanks, Fixed it