Uptime monitor in Elixir & Phoenix: Authorization and authentication
How to handle authorization and authentication in your Elixir/Phoenix project? Read the second part of our uptime monitor tutorial, and learn about building your own solution, utilizing external dependencies, as well as using a native Phoenix solution: mix phx.gen.auth.
Table of Content
- What are authorization and authentication?
- Authorization and authentication in an Elixir/Phoenix uptime monitor: scenarios
- Building authentication and authorization with mix phx.gen.auth
- mix phx.gen Authorization
- Conclusions
What are authorization and authentication?
Welcome to the second part of our Elixir/Phoenix tutorial series. In this episode, we will add authorization and authentication to our application. But first of all, we need to distinguish them - which for some might seem trivial, but I have seen too many people mixing up two of them.
For me that is the most basic distinction:
Authorization is the process of checking if a given user has the access rights to given resources.
Authentication, on the other hand, is the act of verifying a user’s identity.
Authorization and authentication in an Elixir/Phoenix uptime monitor: scenarios
#1 Building own solution
In Elixir we are used to building our own solutions. Apart from Elixir having an active, helpful and growing community, there are still some missing or unfinished dependencies, which would help a lot while developing applications.
Authentication, however, is such a fundamental thing, that there are some powerful solutions available.
Building your solution is the most time-consuming of all possibilities. It takes quite a while to implement and test it. But sometimes, for custom applications, where flexibility and a large number of possibilities to develop the project is really important, your own authentication solution really might be a fit.
#2 Using external dependencies
Using external dependencies is probably the fastest solution if you need something generic. Those ready solutions often also include authorization. One of such solutions is Pow, described as robust, modular and expandable. It generates ready chunks of code and indeed is comfortable unless you need complex login options.
But there is always a cost of using such a powerful tool. You have to dig through a lot of documentation to know what is going on underneath. Pow for example uses a few macros to work. Understanding them can be crucial to adjusting its functionalities to your needs.
And as I said before, Elixir is still a young, developing language. Therefore there is another downside of using external solutions, which is maintenance and keeping your dependencies up to date. You should always have them updated, at least to the last minor change. These may ship with some breaking changes. It may require additional work to adjust your code to these patches.
#3 Using a native Phoenix solution - mix phx.gen.auth
In early 2020 José Valim added a pull request to the Phoenix repository, explaining it with wise words.
“I realized that the best authentication framework is no authentication framework at all.” — José Valim
That is the origin of mix phx.gen.auth, which has been turned into a package by Aaron Renner. mix phx.gen.auth itself is a code generator. Instead of hiding code behind macros and in the complicated dependency code it generates basic files, which should be clear and transparent for anyone who worked in Phoenix. It puts them directly into your application directory, instead of hiding them behind macros and in the dependency code.
mix phx.gen.auth lies between building your own solution and using external ones. It also speeds up the process of building authentication. Thus it gives you the flexibility you need and also doesn’t stop the momentum while building your project. However to understand all these functionalities you still need to read quite a bit of documentation, maybe some article explaining it a bit deeper, and run tests while working with the code.
Building authentication and authorization with mix phx.gen.auth
For the need of this article we will use mix phx.gen.auth to build authentication and authorization. It seems like the best fit - speeding up the process and at the same time leaving us space for some basic explanations and further modifications.
First of all, let's return to our e_meter project. Go to the apps/e_meter_web folder and localize mix.exs file. Then add :phx_gen_auth dependency.
defp deps do
[
...
{:phx_gen_auth, "~> 0.7", only: [:dev], runtime: false}
]
end
Then remember to run the mix deps.get command to fetch dependencies.
Now there comes a tricky part. Since in the last article we separated the Postgres application from the main EMeter application we cannot just simply call the generation command. It will inform us that it cannot find the EMeter.Repo module.
** (Mix) Unable to find EMeter.Repo
This issue is related to the :e_meter_web config, exactly to the generators.
config :e_meter_web,
generators: [context_app: :e_meter]
This config says that the app :e_meter handles the contexts of the application. If there were no context_app specified - the files from the generator would end up in the e_meter_web application.
There is only one issue: mix phx.gen.auth relies on the assumption that there is EMeter.Repo configured in the :e_meter application. That is why the error occurs - we specified our Repo file in the Postgres application.
We have to bypass it somehow. I figured out two ways of doing it after a while of thinking.
The first one that came to my mind was to change context_app to postgres. It generated a huge chunk of code in my postgres app and kinda freaked me out. A lot of copying and possible mistakes. That was a no-go for me.
I also came up with the second idea. What if we tricked mix phx.gen.auth that there indeed is EMeter.Repo in the :e_meter application?
So I created the EMeter.Repo module and pasted the Postgres.Repo code.
defmodule EMeter.Repo do
use Ecto.Repo,
otp_app: :postgres,
adapter: Ecto.Adapters.Postgres
end
Then I launched the command.
mix phx.gen.auth Accounts User users --binary-id
And it worked! Three things still have to be done.
First, the migration created by the generator that needs to be moved. Move it from apps/e_meter/priv/repo/migrations/…._ create_users_auth_tables.exs to apps/postgres/priv/repo/migrations/…._ create_users_auth_tables.exs.
Second, find all occurrences of EMeter.Repo in your code and replace them with Postgres.Repo.
Third, remove the file containing the EMeter.Repo module.
Now you can launch mix deps.get in your main directory and then mix ecto.migrate.
After launching the server you should see the Registration and Login buttons in the top navigation.
If you have an error informing that FormData is not implemented for the Ecto.Changeset while entering the registration page please ensure that you have {:phoenix_ecto, "~> 4.0"} in your e_meter_web dependencies.
A look into the code
The first thing I like doing while looking into someone's code is inspecting and understanding models. This gives me the feeling that I know what I am working with. Let’s look at the user schema.
schema "users" do
field :email, :string
field :password, :string, virtual: true
field :hashed_password, :string
field :confirmed_at, :naive_datetime
timestamps()
end
The only slightly more complicated thing in this model is that there is a virtual field :password and :hashed_password next to it. That suggests that the :password field doesn’t exist in the database and, instead, it is being hashed and stored under the :hashed_password key. The rest of the fields are pretty straightforward.
The second schema model is for user tokens.
schema "users_tokens" do
field :token, :binary
field :context, :string
field :sent_to, :string
belongs_to :user, EMeter.Accounts.User
timestamps(updated_at: false)
end
Token belongs to the user - which is quite understandable. The only thing that draws attention is the :context field. It can contain one of three phrases: session, reset_password or confirm. It can also contain a string based on the user’s email: change:example@email.com. It decides the context of the token - whether it is used in sessions, to reset password or to confirm an account.
:sent_to is an email address to whom the token was sent. The last field is pretty obvious - :token to store a hashed token.
Let’s also take a look at the generated routes.
defmodule EMeterWeb.Router do
use EMeterWeb, :router
import EMeterWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
....
scope "/", EMeterWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
get "/users/reset_password", UserResetPasswordController, :new
post "/users/reset_password", UserResetPasswordController, :create
get "/users/reset_password/:token", UserResetPasswordController, :edit
put "/users/reset_password/:token", UserResetPasswordController, :update
end
scope "/", EMeterWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", EMeterWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
...
The mix phx.gen.auth created quite a lot of stuff for us. It covers all the basic utilities we would like to have in a basic project:
- registration,
- session management,
- user settings,
- password reset.
I strongly recommend going at least briefly through the mix phx.gen_authdocumentation and github page to get at least a basic understanding of what each of these routes contains and how it works with the Authentication context. I won’t explain it route by route in this article because there are already really great articles explaining deeply each of these functionalities. That is why I encourage you to do a bit of research on the topic.
mix phx.gen Authorization
mix phx.gen.auth comes with a built-in authorization. Take a look at the router again. You can spot one authorization-related plug in the :browser pipeline and two plugs in pipe_through’s:
pipeline :browser do
...
plug :fetch_current_user
end
...
scope "/", EMeterWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
...
scope "/", EMeterWeb do
pipe_through [:browser, :require_authenticated_user]
:fetch_current_user, :redirect_if_user_is_authenticated and :require_authenticated_user are plugs imported from the EMeter.UserAuth module. They all have really simple implementations. Let’s take a look at them briefly.
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
end
Firstly fetch_current_user/2 tries to fetch the user_token from session or cookies in the ensure_user_token/1 function. Then it tries to get a user based on the previously fetched token. Once it is finished it assigns the found user under the key :current_user in the conn.
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new))
|> halt()
end
end
require_authenticated_user/2 checks if there is a current user in the conn. If no - then it redirects users to the login path. If yes - it lets the conn pass further. Any route in the scope with this plug will be accessible only for authenticated users.
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
else
conn
end
end
redirect_if_user_authenticated/2 checks if there is a current user in the conn. If no - it lets the conn pass further. If yes - it redirects the user to the signed-in path. In this case, any route in the scope with this plug will be accessible only for unauthenticated users.
With those three basic functions, mix phx.gen.auth handles the authorization. It is really simple and provides you with what you need to create your uptime monitor user base.
Conclusion
In this part of the series, we added simple authentication and authorization to our Elixir/Phoenix application. It was not without some minor issues, but it is nothing we cannot overcome! In the next step, we will try to create data structures for our application. I hope you enjoyed the article and that it can be a helpful resource for some learning!