Blog

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

  1. What are authorization and authentication?
  2. Authorization and authentication in an Elixir/Phoenix uptime monitor: scenarios
  3. Building authentication and authorization with mix phx.gen.auth
  4. mix phx.gen Authorization
  5. 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 is an Elixir/Phoenix authentication method which lies halfway between using ready Phoenix tools and building your own Phoenix authentication solution

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!

Check our latest product - it's based on our experience of managing over 50-people strong company. The tool we're missing as a small company and not an enterprise.

humadroid.io is an employee and performance management software. It's an unique tool allowing everyone to be in the loop - by having up to date info about co-workers, time-off, benefits, assets, helping with one-on-ones, being a go-to place for company-wide announcements.

Check out humadroid.io
Top

Contact us

* Required fields

The controller of your personal data provided via this contact form is Prograils sp. z o.o., with a registered seat at Sczanieckiej 9A/10, 60-215 Poznań. Your personal data will be processed in order to respond to your inquiries and for our marketing purposes (e.g. when you ask us for our post-development, maintenance or ad hoc engagements for your app). You have the rights to: access your personal data, rectify or erase your personal data, restrict the processing of your personal data, data portability and to object to the processing of your personal data. Learn more.

Notice

We do not track you online. We use only session cookies and anonymous identifiers for the purposes specified in the cookie policy. No third-party trackers.

I understand
Elo Mordo!Elo Mordo!