Rolling your own user authentication system can take a lot of time, and doing it incorrectly can be very costly. Thus in recent software history, we’ve seen the rise of third party services, like Auth0, which do that work for the developers.
In the Ruby on Rails framework, I’ve grown accustomed to the Devise library. I don’t just love how easy it is to set up user authentication and related features (password resets, email confirmations, etc.), but I also believe that it can make my applications safer (more on this later). And when compared to a more complete third party solution like Auth0, Devise gives me more control.
So in my new Phoenix application, when it came time to implement a user model with some standard user authentication, I began looking for a similar authentication library. I was basically looking for “Devise, but for Phoenix”.
And what did I find? Well, I stumbled upon this very pertinent blog post by José Valim, the creator of Elixir and a significant contributor to the Phoenix framework. And, well, sort of an ongoing debate. Down the rabbit hole we go.
No Authentication Framework At All
José writes about how he’s not just familiar with Devise, but how he was actually hands-on with creating Devise at his previous company, Plataformatec. He goes on to say, “I have thought about launching ‘Devise for Phoenix’ probably hundreds of times.”
I was very pleased to find someone so qualified discussing the exact topic that I was looking into.
However, I was a little surprised to hear his answer.
… the best authentication framework is no authentication framework at all.
That’s a pretty bold statement. Let’s look at his reasoning (mixed with my thoughts and commentary as well).
Points of Contention
Authentication libraries hide the complexity from the developer
Third party dependencies hide complexity in two ways.
For one, they literally hide code from the developer. You can think of this as explicitly hidden complexity. The separation of user crafted code from third party dependency code serves many purposes (managing dependencies, versioning, etc.), but it is also especially important for user authentication such that the developer doesn’t modify critical sections of code and create vulnerabilities. As a trivial example, consider that the user doesn’t understand the purpose of the
valid_password? method, so they simply delete it. Oops!
And the other way that third party dependencies hide complexity is by, well, doing what they’re intended to do—allowing the developer to utilize the work of others without doing it themselves. This is certainly no small detail. Software has been revolutionized over the last 20 years by this idea of sharing code libraries.
But the more that an authentication library does for developers out of the box, the less the developer is forced to understand what is happening under the hood. This type of hidden complexity might then be referred to as implicitly hidden complexity, which should not be taken lightly when incorporating any third-party library into your application, but one must tread especially lightly when dealing with user authentication.
Authentication libraries are not flexible
If we believe that dependencies hide complexity from the developer, then we will also come to understand that customizing or extending the library will be difficult as a result.
The explicitly hidden complexity is not too difficult to overcome, as if you really need to, you can usually make changes to the library (even if that means creating a fork of a library, etc.), but overcoming implicitly hidden complexity requires the developer to gain a deep understanding of the dependency. And depending on how complex the library is, this will be no easy feat.
Authentication libraries must add complexity to become more flexible
Well-built libraries will offer different options to the developer, wherever possible, that allow them to safely hook into the library, pass options, and make changes. Going one step further, libraries which are also mature will have the added benefit of knowing almost all of the areas where users tend to need customization.
Devise, being both well built and mature, has engineered many different and useful ways to hook into, customize, override, and extend the library. These extensions allow Devise to be surprisingly flexible, but as a consequence, it adds to the overall complexity.
Authentication libraries are difficult to test
As libraries become more complex, it is hard to guarantee that the system will remain secure. Especially so when you start adding customization, it becomes difficult to say, “our library is safe under all possible combinations and configurations.”
The Proposed Solution
With these points of contention in mind, José and his collaborators decided to publish and promote a new tool:
This new tool seems to directly confront the problem of hidden complexity. To do so, it generates all of the files, methods, database migrations, documentation, and tests, and puts everything at your disposal inside of your codebase.
He lists these key points that I’ll also put here for convenience:
- 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.
The motif is keep it simple, and don’t hide anything from the developer. That’s awesome, but the beauty is in the details. Let’s break it down.
Introducing The Generator Approach
Near the end of the article, José refers to
mix phx.gen.auth as a tool which takes the “generator approach”. I find this to be a very useful term when discussing the differences between generating code into your codebase and the traditional third party dependency approach.
Pros of the Generator Approach
Since all of the code that makes up the user authentication system is generated into the codebase, and since the generated system aims to only give users the bare necessities, both types of hidden complexity are eliminated.
Explicitly hidden complexity is gone since the code is placed right in front of the developer, and while implicitly hidden complexity isn’t necessarily wiped entirely, (as the user can still choose to avoid understanding the generated code), it’s certainly not as overwhelming since the library isn’t doing as much for the developer out-of-the-box.
Since both types of complexity are mostly gone, this means testing is easier, and it’s a more flexible solution.
Cons of the Generator Approach
Well for one, it’s not doing as much for the developer out-of-the-box!
If the developer wants more functionality, they will have to get comfortable with the simple framework they were given and build the rest themselves. Again, this is by design and is at the center of the argument that José Valim is making—the developer should understand their user authentication system.
Next drawback—I’ve already talked about how one of the benefits of traditional dependencies is that, by explicitly hiding the code from the developer, it can help prevent them from editing critical sections and creating vulnerabilities.
The generator approach turns that idea on its head and gives all of the responsibility back to the developer. All of the code is there, visible in the codebase, and free for the developer to bring to ruin (even if there are good intentions). To combat this issue, in an effort to steer developers away from misguided edits,
mix phx.gen.auth adds very visible, cautionary comments in the code warning users not to mess with the important sections.
This workaround sort of gives me the heebie-jeebies. It’s true that developers should be careful and should be reading comments, but putting one of the last lines of defense for your authentication system in a comment rubs me the wrong way.
The last concern has to do with maintenance, security vulnerabilities, and versioning.
With generators, if there is a new common vulnerability or exposure, you can’t just update the dependency like you might with
mix deps.update, etc. And you can’t simply rerun the generator, since you’ve probably already made extensions and customizations to the generated code.
Instead, the proposed solution is for the package manager to notify users of new updates and let them act accordingly. Developers can also rely on static analysis tools like
diff.hex.pm. However, these solutions are much more hands-on than with traditional dependencies.
So Where Do We Land?
I still love Devise, and because of the nature of my work and the applications that I work on I think it makes my applications safer. That doesn’t mean that third party authentication libraries are always the right choice.
I tend to work on small-scale web applications, and if I had to roll my own user authentication system for every application, well, that would be all that I spend my time doing. Say goodbye to making things look pretty, adding testing coverage, and worst-of-all, bye-bye blog posts. I would be forced to write more lines of user authentication code, which would increase the risk of making a mistake. As for maintenance, it would be incredibly difficult to make sure that each manually-rolled authentication system is up-to-date against the latest and greatest vulnerabilities.
I also believe that Devise is perfectly tailored to the Ruby on Rails framework. There’s already the “magic” of Rails, and lots of business logic which is hidden from the developers—Ruby on Rails developers are already managing hidden complexity. With that in mind, a complex user authentication library like Devise fits perfectly into the developer’s existing mental model.
But should every application use and rely on a third-party authentication system?
Though few and far between, there are applications that may outgrow what a third-party authentication system can offer. That might be because the library isn’t flexible enough, or you might just feel strongly about handing over business logic to a third party.
An authentication system is a keystone of your application’s security, and choosing how to build one or what dependency to use is an important decision to make, and one that will vary case to case, application to application.
Some things to consider when choosing an authentication system:
- Size of application?
- How critical is the application’s security?
- Amount of resources available (time, money, etc.), can you afford to build your own?
- Do your engineers have the training required to write the security code?
- Maintainability—how important is it, and how many resources can be devoted to maintenance down the road?
Depending on the requirements, it might be the case that you need to build your own authentication library (although unlikely).
And one last thing that might impact your decision—what framework are you using?
Phoenix is aiming to be a framework which does not hide business logic.
In José’s words, he says “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.”
If that is the mental model that José is trying to build for Phoenix developers, then Phoenix applications are going to require more care, more maintenance, and force developers to have a deeper knowledge of their application. And I don’t necessarily think that’s a bad thing! But it’s definitely something to consider when choosing Phoenix as a framework for a project.
How do you feel about authentication libraries? What thoughts go through your head when you’re choosing dependencies for an application? Questions, comments, concerns? Let me know!
Loved the article? Hated it? Didn’t even read it?
We’d love to hear from you.
Thanks for this VERY IMPORTANT write up. It is exactly the thing I was working and thinking through today!
My thoughts are that I would DEFINITELY be rolling my own authentication system, and not using Devise. My problem with Devise is exactly what is mentioned in the article – that it hides way too much of the code from the developer, which means making any kind of customizations means reading documentation rather than writing code, and with each customization, we’re getting further away from understanding what’s actually going on.
For me, I don’t feel comfortable having such a critical part of the app’s code be almost completely hidden from me as the developer. It leaves me feeling very uneasy knowing that as the application grows and with it, as the need for customizing parts of the login system grows, IF it’s too complex to do with Devise, I would have to spend hours understanding Devise or search stack overflow OR even worse..have to then, on a live app with potentially 1000s of users, switch to implementing my own authentication system from scratch. Both scary prospects.
SO in my case, it’s best to create my own authentication system, from scratch, right from the get go, understanding and knowing EXACTLY how all of it works. Sure it’ll take a lot more time than using something like Devise, but in the long run, I always know customizing it will be MUCH lesser work.
Leave a comment