Search code examples
ruby-on-railsdeviserestful-architecture

How to authenticate based on devise encrypted password?


I have two Rails apps using devise for authentication: a website and an API. Even though I'm the owner of these 2 apps, for many reasons, I wanted complete separation between the two. When a user signs up on the website, an account is automatically created on the API side. The biggest problem I have is keeping the users table in sync between the two apps. I ended up with a bunch of RESTful API callbacks on the website user model which create/update the API user.

The second part is calling the API on behalf of the user logged into the website. Problem is that I don't have the user's decrypted password so I can make the API call. All API calls use basic HTTP authentication. The passwords are hashed into the database and cannot be decrypted (which is a good thing, thank you devise). Because I keep all users columns in sync between the 2 databases, my website users encrypted_password column is identical to my API users encrypted_password column.

So, what options do I have for solving this? I'm thinking either modifying the API such that an admin call for each regular call takes an admin username and password and a user ID (to get that user's transactions for example). Or, implement a shared database between the 2 apps - but I have a lot of associations between users and other models... how would indexing even work?! Or, hijack the devise authentication and compare encrypted_password (from my website) to encrypted_password (to my API) - since they are the exact same; but this opens a security can of worms. Or, create keys authentication and generate unique GUIDs for users... but then that would be just as bad as having decrypted password stored in a database. I hate all these solutions. Perhaps someone has a better idea?


Solution

  • When you authenticate a user in Devise it takes the plaintext password and combines it with a pepper and passes it through bcrypt. If the encypted password matches the value in the DB the record is returned.

    The pepper is based on your rails app secret. So unless the two apps have the exact same secret then authentication will fail even if you have the correct encrypted password. Remember that the input to bcrypt has to be identical to give the same result. That means the same plaintext, salt, pepper and number of stretches.

    You're correct in that sharing the passwords back and forth is not a solid solution. You are also correct in that using UUIDs instead of a numerical auto-incrementing id is part of the solution.

    What you can do is implement your own authentication provider. The Doorkeeper gem makes it pretty easy to setup your own OAuth provider (or you can use Auth0 if using an external service is acceptable).

    The web app would use Devise + OmniAuth to authenticate users against the authentication provider and use the credentials returned to identify the user in the web application.

    For the API application I would use Knock for JWT authentication. Where the API server proxies to your authentication server via the oauth gem.

    However you should consider at this point if your web and API app really should be running on separate DBs. Keeping writes in sync across both DBs can be quite the task and your should ask yourself if your really need it at this stage.