Why You Shouldn’t Roll Your Own Authentication

Development

Reading Time: 7 minutes

“Should I roll my own authentication?”

Given how easy it is to build an authentication system with Rails’ has_secure_password and the authenticate method (as shown in Hartl’s tutorial), why would you jump straight to a gem like Devise, which is hard to understand and customize?

In this article, I hope to lay down the case for why I think the answer to my first question is, “No, you shouldn’t roll your own authentication for a production app.”

First, let’s take a look at the authentication code from the Hartl tutorial. In app/controllers/sessions_controller.rb, we see the create action defined like so:

def create
      user = User.find_by(email: params[:session][:email].downcase)
      if user && user.authenticate(params[:session][:password])
        # Log the user in and redirect to the user's show page.
      else
        # Create an error message (similar to 'invalid email/password combo')
        render 'new'
      end
    end

In the ‘happy case’, this works by first ensuring a User record can be found for the given email and then calling the #authenticate method on the User object.

Now because the #authenticate method relies on the bcrypt hashing algorithm, it will take a decent amount of time (from a computer’s perspective) to run. This is a good thing. If you have a good password, it’ll dissuade malicious actors from brute-forcing their way in.

What Happens If user Is Nil?

Because of how the if statement is constructed, #authenticate won’t actually be called if User.find_by(...) returns nil. If #authenticate isn’t called, we can be reasonably sure that the server will render a response quicker than it would if #authenticate was called.

In other words, if a given email doesn’t exist in your database, your server response time when you post to the #create action will be measurably faster. Thus, by analyzing the time it takes for your server to render a response, a malicious actor can begin to assemble a list of valid emails on your system.

Let’s try it out! We’re going to get the tutorial app running locally and see if we can discern any difference in server response times between a valid and invalid email address.

  1. Clone the tutorial app or any app that uses this particular style of authentication.
  2. Start the server with rails s.
  3. Create a user account if you need to via the sign-up form. For our example, we’ll use sid@example.com as the email.
  4. In a separate terminal window, do:
curl localhost:3000/signin --cookie-jar cookie | grep csrf

When we post data to the #create action in the next step, Rails’ protect_from_forgery method will ensure that we also post a valid authenticity token and cookie. So with the above curl and subsequent grep, we’re downloading the sign-in page, storing the cookie in a file named cookie, and then searching for the authenticity token. The output should look something like this:

% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 
100 4361 100 4361 0 0 24525 0 --:--:-- --:--:-- --:--:-- 24638 
<meta content="authenticity_token" name="csrf-param" />
<meta content="jMeQnPStWoDP5Y5CcTCWgf7nBnja6fKYH7CO427WCZQ=" name="csrf-token" />

Now that we have the cookie and an authenticity token, we can post data. In this step, we’ll measure how long it takes for the server to respond to a valid email/invalid password combination. In your terminal window, do:

curl -X POST -F 'session[email]=sid@example.com' -F 'session[password]=something' -F 'authenticity_token=jMeQnPStWoDP5Y5CcTCWgf7nBnja6fKYH7CO427WCZQ=' localhost:3000/sessions --cookie cookie

As you can see, I’ve used sid@example.com as the email address, and I’m also passing in the authenticity token from step 4.

Per our discussion earlier, because this is a valid email address, we expect that User.find_by(...) will not return nil and that #authenticate will be called. In your server log, note the response time. In my case, it was around 130ms. It won’t be the same every time, but it should have a pretty well-defined range, which you can average.

Let’s see how long it takes for the server to respond to an invalid email.

curl -X POST -F 'session[email]=nonexistent-user@example.com' -F 'session[password]=something' -F 'authenticity_token=jMeQnPStWoDP5Y5CcTCWgf7nBnja6fKYH7CO427WCZQ=' localhost:3000/sessions --cookie cookie

Again, note the average response time. You should notice that in this instance, the response time for this request is consistently lower. As expected, #authenticate is not called because User.find_by(email: 'nonexistent-user@example.com') returns nil.

If you don’t want to look at the server logs, you can also estimate response time by prepending time to your curl commands. For example:

time curl -X POST -F 'session[email]=nonexistent-user@example.com' -F 'session[password]=something' -F 'authenticity_token=jMeQnPStWoDP5Y5CcTCWgf7nBnja6fKYH7CO427WCZQ=' localhost:3000/sessions --cookie cookie

Using this method, I can guess if a given email address exists in the database.

In a real site that implements this, you won’t be able to check server logs. On top of that, internet response times are pretty random. But that doesn’t mean timing analysis is not possible. You can use statistics to filter out random noise and ascertain with a reasonable degree of confidence that one given request is slower than another.

The way to fix this is to ensure that the controller takes a fixed amount of time to render a response irrespective of whether the user exists or not. So you could do something like this:

user = User.find_by(email: params[:session][:email) || FakeUser.new

The FakeUser class implements #authenticate by comparing the hash of a passed-in password to a default hash and returns false.

Is This Good Enough?

Even if you implement the above securely, you’ll probably need to add a few more features as your app grows in order toimprove the authentication UI and/or further secure your authentication:.

  1. Allow the user to recover from a lost/forgotten password.

    Let’s take a look at Railscasts episode 274 where we learn how to implement the reset password functionality. In PasswordResetsController#create, we see a familiar pattern:

    user.send_password_reset if user # equivalent to user && user.send_password_reset
    

    If the user doesn’t exist, then the #send_password method, which generates a token and saves it to the database, is not called. This would likely result in a time difference between a valid and an invalid form input, again allowing someone to guess if a given user exists or not.

  2. Implement a lock out system, to prevent automated attacks, either from a single IP or a set of IPs.

  3. Implement an API authentication scheme.

  4. Implement OAUTH.

  5. Implement two (or more) factor authentication, password complexity requirements and more.

Each one of the above features represents a potential security hole in your system once it is implemented. If your primary concern is the business logic of your app and not its security, I think it’s in your best interests to go with a proven solution, like Devise.

The Pros and Cons of Using a Gem like Devise

People are always looking for vulnerabilities. When vulnerabilities are found, a patch is released. Because of this, you can reduce the amount of time and thought you put into security and focus on the business logic of your app.

Devise implements many of the authentication-related features described above, and moreover does so in a secure manner. As an exercise, I’d recommending trying the above timing analysis on an app that uses Devise.

That being said, Devise != rainbows and unicorns. To customize Devise, you have to spend time to understand how it works. The documentation might not be the best, which can be super frustrating. You’ll also have to ensure that you and your team have a habit of updating the gem when patches are released. Plus, it’s one more dependency in your codebase.

And of course, you still have to be vigilant about potential security issues. Just because authentication is secure doesn’t mean sensitive information can’t be extracted from your app another way. For example, a “sign up” or “registration” page sometimes offers an easier path to guessing an existing user’s email, because of the common message which says ‘email address already exists’ or something similar. Captcha systems can help in prevent automating this sort of guessing game.

Another example I recently saw was a piece of code that was using MD5 to generate a token.

Conclusion

To recap, it’s easy to introduce holes when it comes to authentication and authorization if you’re not familiar with security or don’t spend much time looking for weaknesses. Rolling your own authentication is still a wonderful learning experience though, as well as a way to explore your and others’ ideas.

But if you want a proven solution, use Devise or a similar gem or at least one that’s under active development and regularly poked, prodded, and patched.

Otherwise, keep a few general security tips in mind:

If you roll or have rolled your own authentication, I’d like to hear from you. What drove you to make the choices you did? How did it work out? And would you do it again?

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.

  • Wholeheartedly agree. If it was only authentication (login/logout functionality), maybe. However, unless you only provide authentication through external providers (e.g. social networks), you will going to have to implement the whole account management process, which includes all of the things you’ve mentioned.

    “But I just want something simple”. No, for authentication & account management you don’t, because here “simple” usually leads to “less secure”. There are so many well-known attacks, and you don’t want to be repeating the same mistake.

    The only reason I can think of where you need to build your own solution is (a) if you’re building a JSON API, and (b) if you’re using another web framework which isn’t Rails. As far as I know, none of Devise/Sorcery/Authlogic work with either of those (correct me if I’m wrong). But now thanks to Rodauth both of these scenarios are solved.

    Great writeup, I like when we sometimes talk about what we shouldn’t do.

    • There’s another reason even if you are using Rails. You’re not using one of the two supported ORM solutions Devise support, which would be the case for Sequel for example. Also, I guess most applications are not really hackers’ target. There are simple techniques around the kind of attack described here (which is also hard to perform on practice because, at least with Sequel and PG, from my experiencing, finding an user and checking the password would usually be so fast that the average time of the request and variance would be about the same for valid and invalid e-mail addresses). Your application could simply sleep for some random large time (1, 2s) on authentication failure to prevent brute force attacks but on the other side this could make it easier to perform a DoS attack. If security is really important for your business which is likely to be a target from hackers, you’d have to have a great security team anyway or hire an specialized company to perform some audits from time to time. You’d probably have some software outside the web application that would detect some attack patterns and automatically drop network packages from the attacker and other advanced techniques… Just because you are using a proven authentication framework it doesn’t mean it’s actually safe. But of course the that is the feeling. The thing is that security has a cost involved. It’s either very important for your business or it’s not. You’d have to evaluate the impact of a successful attack and see how much your company would be willing to invest on security given the estimated impact. Of course this does not apply exclusively to the authentication part of the application.

      Anyway, this is not how it’s usually implemented in most applications I’ve seen. Usually it works this way:

      digested_password = digest params[:password].to_s[0..MAX_PW_LENGTH]
      user = User[email: params[:email].to_s[0..MAX_EMAIL_LENGTH], digested_password: digested_password]
      if user …

      Usually the part that handles params extraction and sanitization is abstracted somewhere in the application.

      • Siddharth Krishnan

        Thanks for reading Rodrigo & Janko.

        Rodrigo, a few points:

        1. Inserting random sleep doesn’t help much because it is possible to statistically eliminate random noise from a given sample of data (assuming you collect enough).

        2. I forget the name of the paper, but a team of security researchers found that it is possible over HTTP to differentiate response times on the order of microseconds. Actually, here’s the link: http://www.cs.rice.edu/~dwallach/pub/crosby-timing2009.pdf

        3. Good point about Devise only supporting the 2 ORMs.

        4. Re: your application being important to hackers – it might not apply for the example I used in the article, but “hackers” work (in my knowledge) by using scripts that just scan over a range of IPs and try to detect vulnerabilities. You don’t really have to be explicitly targeted by them to be taken advantage of.

        5. Re: having a great security team – in a sense this is what I think of Devise as (atleast for whatever their responsibility is). They have years of experience and are frequently audited.

        6. For timing attacks in general, I recently learned that it is better to resist them in the network layer – check out the Rack Attack gem.

        7. Agree that Devise is not a guaranteed safe solution.

        • I’d love to see someone demonstrating how they can measure timing over regular internet TCP servers, specially when GC is involved (Java, Ruby, Python), in the order of microseconds (bcrypt will take something about 50us to generate a hash) reliably. I find it really hard to believe that a generic algorithm would be able to apply such kind of attacks to random servers (it’s already hard when you know exactly how the authentication requests work). In a regular Rails application running over a regular server I’d be really surprised to see someone able to measure the specific code timing differences reliably in the microseconds range.

          With regards to a random sleep I didn’t get your point. Do you have any article demonstrating that we can filter the noise from random?

          With regards to your point 5 what I meant is that authentication is just a small part of security. If security is really important to your application, you should probably have a great security team taking care of it. Relying on Devise’s team would at most protect against authentication attacks but would leave all others uncovered. If you have a security team, it’s probably easier for them to audit some simple code that does just what it needs than to audit tons of code which may be mostly ignored by your application but could be exploitable. It’s always about trade-offs…

          With regards to 6, I agree, and that’s why I think it doesn’t make sense to try to protect against all edge cases in the application itself when they could be better handled in the network layer (although I guess most robust solutions are not implemented in Ruby). You’d have to protect against DoS and DDoS anyway so I guess brute-force attack would be automatically mitigated by such protections, which are actually much harder to implement.

          Security will always be a hard topic. I just think that people might be often fooled to think that they are safe just because they are using Devise or any other popular authentication library.

          • Siddharth Krishnan

            Hey Rodrigo, here’s an article that goes over how random delays can be filtered out – search for ‘random delays’: http://blog.ircmaxell.com/2014/11/its-all-about-time.html

          • I know there are mathematical theories saying that given enough samples one could eliminate random, but I’m simply not convinced that this could lead to timing based attacks that are faster than pure brute force attacks. Also, comparing two equal strings (the worst) of 128 bytes in Ruby takes about 5 or 6 us in my local computer and it’s even faster in our servers. One of those articles claim they have figure out some techniques that could allow them to get 15-150us precision or something like this. That means that a regular Ruby string comparison wouldn’t be vulnerable over the Internet even in the best cases they described (and I hardly actually believe they would be able to get those 150us precision either in a real Internet server as there’s simply too much entropy affecting the requests, specially for Ruby servers). So, while I may accept that theoretically it could be possible to apply such timing attacks against real servers I’m not really buying this as something practical nowadays. I guess that even if you tried to apply a timing attack against a local Rails app I’d be surprised if you would be able to successfully figure out the password faster than by attempting to perform a pure brute force attack…

  • Joel

    Cool link bait and FUD-spreading. I guess if you disagree with the choices made by the various libraries you mentioned, you should just swallow that bitter pill and choose one of those anyway? Or make your own and wait for it to get popular?

  • Andrew Nagi

    Security considerations must be balanced against other considerations. Devise is not a “slam dunk”, and rolling your own is often the better choice.

    First, let me address the vulnerability in Hartl’s code uncovered by the timing attack you describe, as that is the pretext for your title and conclusion.

    in the context of Hart’s application (a twitter clone). How long would it take to brute force a single user over HTTP? Maybe days, maybe weeks? Wouldn’t it be easier to sign up to the twitter clone and use social engineering to get the emails of other users? If I did get an email, how useful is that information when Hartl is using BCrypt? You recommend Devise as a mitigation, but I believe that the password recovery stuff in devise will respond “Email not found” or “We’ve sent an email to your address” unless you tell it not too!

    So sum up I would argue that for the majority of Rails apps, the code you have not uncovered a vulnerability; so I am not persuaded by that argument. The article was interesting, well presented and a nice initiation to the idea of timing attacks so I applaud that. I just disagree with the conclusion.

    My biggest fear is not security, it’s complexity. Over the long hall complexity problem cripple more apps than security problems ever will. I’ve dealt with application level security issues, but the app lived on. Complexity is terminal. Devise introduces complexity unashamedly.

    I’ve found that Devise is superb at speeding me up in the short term, but might glue me down in the long term depending on future requirements. That’s a better yardstick I.M.H.O.

    • Siddharth Krishnan

      Thanks for reading, Andrew.

      First, I wholeheartedly agree that security considerations must be balanced against others. This probably applies not only to security, but most other areas of software design.

      I’d appreciate an example or two of how Devise has glued you down – I do agree that it is complex. As an aside, its curious that in security that you sometimes have to add complexity or do things less efficiently to resist attack.

      That being said, I do think a gem like Devise which is regularly screened for vulnerabilities is the lesser evil if security is not your primary domain (and you deem working on business logic more important).

      When a vulnerability is found in Devise, it is announced publicly and you have a chance to patch your app. Additionally, you are relying on years of knowledge and experience (as evidenced by their change log).

  • Oleg Sobchuk

    Devise?! Are you crazy? You can create authentication in one single model! My Session model takes 20 strings! Devise simple for start, but hard for customization!
    For me, it’s bad idea!

    • Devise is a highly reliable sink for as many resource-weeks as you care to throw at it. It’s the closest thing the Ruby world has yet devised to JEE, on multiple levels — you have to understand ruddy near everything to be confident you understand anything well enough to trust it to secure your data.

  • If you are concerned about timing attacks you should know that Devise would allow such attack in theory to identify a valid account because it performs a database query using some e-mail or username check only and then, if found, it will compare the password. This means it will take longer if the e-mail or username is valid, leaking this information. Also, I suspect a database query would be usually altogether with the ORM type conversions would take a different amount of time to complete depending on whether it finds a result or not. This could also allow some information to be leaked in theory… So, I’m not so sure that timing based attacks are a valid reason for using Devise over the alternatives… I guess there are many other parts of Devise that would be vulnerable to timing attacks on theory…

  • James Scott

    Sid,

    Thanks for the article. I agree with many points you have made here, however I have found the current mix of authentication and authorization gems frustratingly embedded in Rails MVC mediocracy. Most of the apps I have written were focused presenting secure and consolidated views of resources on other systems, and not a local db as the main data source. I’ve looked a Devise several times and backed away each time; but acknowledge it to be the best available gem for authentication. Sadly, I have no such best candidate for authorization.

    Discussion related to authentication and authorization are good to have. I’d like to see real examples, so I’ll have submitted my core ideals on authorization and authentication in a demonstration app here: https://github.com/skoona/SknServices

    I’d welcome your comments of pull requests; just ignore the UI — I was focused on singling out the internals. meanwhile thank you for an excellent recap.

    James