Multi tenancy with Devise and ActiveRecord’s default scope

multi tenancy

Multi tenancy with default scope

Multi tenancy in a Rails application can be achieved in various ways, but my favourite one is using ActiveRecord’s default scope  as it’s easy and provides good security. Essentially, the core of this technique is to define a default scope on all the resources owned by a tenant, or account. For example, say you have a tenant model named Account, and each Account has many users. The model User could define a default scope as follows:

Do see this screencast by Ryan Bates for more details on this model of multi tenancy.

The problem with this technique is that it often gets in the way of authentication solutions like Devise, which happens to be one of the most popular ones. One common way of implementing multi tenancy with Devise is using subdomains, as suggested in Ryan’s screencast; this works well because it’s easy to determine the tenant/account by just looking up the subdomain, regardless of whether the user is signed in or not. There are cases though when you don’t want or can’t use subdomains; for example, an application that enables vanity urls with subdomains only for paid users while using standard authentication for non paid users. In such scenario your application needs to implement multi tenancy both with and without subdomains.

So if you need to use the typical Devise authentication while also implementing the multi tenancy with the default scope to isolate the data belonging to each account, this combination won’t work out of the box. The reason is that the user must be already signed in, in order for Devise’s current_user to be defined, and with it – through association – the current account:

If the user is not signed in, Account.current_id cannot be set, therefore the default scope on the User model will add a condition -to all the queries concerning users- that the account_id must be nil. For example when the user is attempting to sign in, a query like the following will be generated to find the user:

As you can see it looks for a user with account_id not set. However, it is likely that in a multi tenancy application each user belongs to an account, therefore such a query will return no results. This means that the user cannot be found, and the authentication with Devise will fail even though a user with the given email address actually exists and the password is correct. This isn’t the only problem when using Devise together with default scope for multi tenancy without subdomains. Each Devise feature is affected:

  • authentication: the first problem you won’t miss when enabling default scope in an application that uses Devise for the authentication, is simply that you won’t be able to sign in. This is because the user cannot be found for the reasons explained earlier;
  • persistent sessions: once you get the basic authentication working, you will soon notice that the session is not persisted across pages. That is, once signed in you will need to sign in again when you change page in your application. Here the default scope gets in the way when retrieving the user using the session data;
  • password recovery: there are two problems caused by default scope to the password recovery process. First, as usual the user cannot be found when supplying a valid email address; second, when reaching the ‘change my password’ form upon following the link in the email the user receives, that form will be displayed again upon submission and the user won’t actually be able to set the new password because this form will be displayed again and again. Some investigation when I was trying to fix showed that the reason for this is that since the user cannot be found in that second step of the process (because of default scope, of course), the token will be considered invalid and the password recovery form will be rendered again with a validation error;
  • resending confirmation email: this is quite similar to the password recovery; first, user cannot be found when requesting that the confirmation instruction be sent again; second, token is considered invalid and the confirmation form is displayed again and again when reaching it by clicking the link in the email.

In order for Devise to find the user in all these cases, it is necessary that it ignore the default scope. This way the query like the one I showed earlier won’t include the condition that the account_id must be nil, and therefore the user can be found. But how to ignore the default scope? As Ryan suggests in his screencast, it’s as simple as calling unscoped before a where clause. unscoped also accepts a block, so that anything executed within the given block will ignore the default scope

So in order to get the broken features working, it is necessary to override some methods that Devise uses to extend the User model, so that these methods use unscoped. I’ll save you some time with researching and just add here the content of a mixin that I use for this purpose:

See the use of unscoped. Then, simply extend the User model with this mixin (which I keep in the lib directory of the app):

That’s it. You should now have Devise working just fine with the default scope for multi tenancy in your Rails application, without subdomains. While I was investigating these issues I was wondering, would it be a good idea to update Devise’s code so to ensure it always uses unscoped by default? In my opinion this wouldn’t affect the existing behaviour and would make this way of doing multi tenancy easier without having to override any code. What do you think? If you also know of a quicker, easier way of achieving the same result, do let me know!




Have your say!

Please see my comment policy if this is your first time here or if you have any questions regarding comments.