An upcoming authentication solution for Phoenix
I am no stranger to authentication. A little more than a decade ago, I worked with my colleagues at my previous company, Plataformatec, to create a flexible authentication solution for Rails called Devise. As time passed, Devise became the de-facto authentication solution for Rails and one of the most used Rails packages, with more than 71 million downloads at the time of writing.
At some point I changed career paths and started to focus exclusively on developing Elixir and contributing to its ecosystem (Phoenix, Ecto, etc). Since I was involved in both Devise and Elixir, I was often asked: when will you launch Devise for Phoenix?
I guess the answer is now. Kind of.
A long time in the making
I have thought about launching “Devise for Phoenix” probably hundreds of times. I had long conversations with Chris McCord (creator of Phoenix) and co-workers about this. Helping Phoenix users get past the burden of setting up authentication can be a great boost to adoption. At the same time, I never found a proper way to approach the problem.
Luckily, the Elixir/Phoenix community stepped in and tried different approaches: Coherence, Pow, Guardian, and many more.
Every time a new solution came out, I would study the source code. Often making security audits along the way and reporting bugs upstream. While working with different clients, I would talk to them and collect feedback on what worked and what didn’t. And more time passed, the more I realized that best authentication framework is no authentication framework at all. This is especially true for Phoenix applications.
Since Phoenix v1.3, Phoenix makes a big distinction of what is part of your web application and what is part of your business domain. Drawing these lines are important because, while I am perfectly ok with delegating a big chunk of my web application control to a third-party library, I am very unwilling to compromise when it comes to the business domain.
For example, in earlier Devise versions, we would generate a database migration file like this:
create_table(:users) do |t|
t.database_authenticatable null: false
t.recoverable
t.rememberable
t.trackable
end
When I look at this file, I can’t answer how my data will look like. It is hiding too much from me. Then a Devise model would look like this:
class User < ApplicationRecord
devise :database_authenticatable, :recoverable, :rememberable, :trackable
end
It is extremely unclear which functionalities my business domain object provides, how they relate to each other, etc. The issues with hiding most of the authentication complexity behind an authentication framework became more apparent when people wanted to customize how Devise worked. For this purpose, we allowed developers to copy Devise’s default controllers and views to their application. We added many callbacks and many configuration knobs. Looking at Devise’s API today, it has more than 35 different settings only at the root level. The devise
call above accepts its own options too.
While this made Devise more flexible and general purpose, it also made it more complex. A complex codebase is harder to be audited, which is important in authentication systems. Furthermore, the existence of too many options and customization hooks makes it extremely hard to guarantee that the authentication system will continue be secure under all possible customization combinations.
With time, I realized that what I want from an authentication system is for it to be as straight-forward as possible. When considering an authentication system for a server-side MVC application, I don’t want to hide my model/domain code under a framework/library. In particular, I don’t want to see my Ecto (Elixir’s database library) schema fields hidden behind a macro:
defmodule User do
use Ecto.Schema
schema "users" do
authentication_fields()
end
end
When it comes to controllers, views, and templates, they belong directly in my web application, as I may want to customize the user interface and the user experience.
Therefore, with all things considered, there is very little space for an authentication framework. So what does it mean? Everyone has to write their authentication system from scratch?
Not really. My proposed solution is to provide generators to inject all relevant authentication code into your application.
The authentication system demo
About 2 months ago I decided to handwrite a simple and secure authentication solution on top of a Phoenix application. I did a specification of how the system would work and e-mailed Griffin Byatt, the creator of Sobelow, a security-focused static analysis for Phoenix. After some back and forth and validation on the security aspects from Griffin, I was quite satisfied with the design document and I had a complete picture of how the authentication system would work. In particular:
-
For the password hashing, we can simply rely on the outstanding work done by David Whitlock on the comeonin libraries
-
For cryptography at the HTTP layer, the primitives available in Phoenix and Plug were too low-level. So we have worked on releasing Plug v1.10, which provides high-level API for signing, encrypting, as well as built-in support for signed and encrypted cookies
-
Then all that is left is to write plain and boring Phoenix application code :-)
I have written the authentication system as a pull request to a bare Phoenix application. Code reviews and security audits are greatly appreciated. The code is also licensed under Apache 2, so anyone can give it a try right now if they wish to.
Here are some interesting tidbits about the system:
-
It provides a registration page with session-based login/logout, account confirmation, password reset, and remember me cookies. You can also safely update your e-mail (it requires confirming the new address to become effective) and safely update your password - both operations require the current password.
-
The system uses only two database tables: one with the user information and another with all user tokens.
-
Currently there is no integration with an e-mail or SMS library. This will likely vary a lot per application, so we currently only log messages to the terminal. Developers will have to bring their favorite libraries for this. We have listed some options in the generated code.
-
The business domain code (the Phoenix context plus Ecto schemas) is only 340LOC which attests to the power of the platform. With docs, it jumps to roughly 600LOC. Note the code has been formatted by the Elixir formatter (so no code golfing).
-
The five controllers take only 230LOC. They are all relatively straight-forward and simply handle the return types from the business domain. The templates take 168LOC altogether - which you will most likely customize anyway.
-
The authentication system has 100% code coverage. The tests altogether take about 1100LOC. They are by far the biggest chunk of the code.
It took me roughly 7 working days to implement the complete system. This does not take into account the time spent designing the system. I expect it to take longer in greenfield projects, especially if they don’t have a lot of experience writing their own authentication systems. This highlights the importance of having such solutions readily available.
Next steps
At the moment, Aaron Renner from DockYard is working on converting the pull request into an actual code generator called mix phx.gen.auth
. The generator will ship as a separate package that you can bring into your apps to generate the authentication system.
The generator is meant to be a simple and straight-forward starting point. If you have basic needs for authentication, it will most likely do the job. If you have complex needs, then I believe there is no library that will take you all the way, so a solid foundation trumps a complex solution. If your goal is third-party integration, then look at uberauth or assent.
I am also aware that generating the whole code into user applications comes with downsides. After all, the user can easily modify the code, making it unsafe. To help balance that, there are code comments whenever important decisions related to security were taken. The tests also help prevent unintentional regressions.
The other concern is about security vulnerabilities. If there is a vulnerability, you can’t simply update the code to get the latest. We plan to address this by retiring vulnerable package versions and relying on the Hex package manager to notify users. On the positive side, because the system is dead simple, we hope it will be mostly safe from vulnerabilities. Tools like phoenixdiff.org
and diff.hex.pm
can be used to track how the authentication system will evolve over time.
These trade-offs may not be everyone’s cup of tea. If that’s your case, then you can use the other tools available in the community. But if someone were to ask me which approach they should take for authentication today, I would personally go with the “no authentication framework” option.
If you prefer the generator approach but you’re not satisfied with the choices I made, David Whitlock (comeonin’s creator) also wrote his own authentication generator more than 2 years ago, which you can also give a try.
Stay safe and have fun!