One thing I stress about Authority is that it’s ORM-neutral. It doesn’t make any assumptions about how to look up a user or what determines their permissions; it just gives you a convenient place to answer questions like “can this user do X?” and to enforce that policy.
The reason for this hands-off approach is that we originally designed Authority for applications where there would be no user model. For authentication, we built a single-sign-on application and a gem that enabled easy integration.
The end result was a really nice single-sign-on workflow and a really flexible authorization library. In case you have a similar use case in mind, here’s the basic strategy we used.
Our single-sign-on strategy
1. Single sign-on app in the same domain
First, we built a single-sign-on app. All the clever pseudonyms I can think of are taken, but let’s pretend it was called Gatekeeper.
Gatekeeper and all the applications it works with live on the same domain; for example,
personnel.example.com. This is important because it means they can share cookies.
Gatekeeper serves as a single place to do the following:
- Sign in, reset passwords, etc, using Devise. (We don’t allow signing up; a manager has to create a user’s account.)
- Maintain the list of applications it protects
- Maintain a list of active users
- Maintain a list of assignable roles for each app
- Assign users roles within those apps and roles within Gatekeeper itself. Gatekeeper’s roles determine who can assign what roles to others
Together, this means that creating users, assigning them roles in apps, and inactivating them can all be handled by non-technical managers.
2. A middleware gem for integration
Once we had Gatekeeper, we needed to make other apps use it. We wanted integration to be painless, so we packaged everything we needed into a gem, which I’ll call
gatekeeper_client to a project’s Gemfile does the following:
- Sets up a middleware to protect the app. Every request is authenticated with Gatekeeper. (Well, almost. The host app can configure paths to skip authentication, and assets are skipped by default.)
Provides a controller method and view helper method called
gatekeeper_user. This is done via a module that we include into
When a user makes a request to
warehouse.example.com, their browser automatically sends along the cookies for
example.com. That kicks off this process:
The middleware checks whether they have a Gatekeeper cookie.
If there’s no Gatekeeper cookie, it redirects them to Gatekeeper to sign in.
- Once they sign in, Gatekeeper redirects them to the URL they were trying to access, starting the process over.
If there’s a Gatekeeper cookie, it makes a request to Gatekeeper and passes it along.
Gatekeeper checks whether the cookie is for a current session.
- If not, it tells the middleware, which redirects them to sign in.
If so, it checks whether the user has any roles in the requesting app.
- If not, it tells the middleware, which redirects to Gatekeeper’s “forbidden” page: the user has no business in this app
- If so, it tells the middleware what it knows about the user.
- Gatekeeper checks whether the cookie is for a current session.
- If there’s no Gatekeeper cookie, it redirects them to Gatekeeper to sign in.
In the success case, Gatekeeper’s job ends when it determines that the user is signed in and has at least one role in the requested app. Its reply is essentially:
"Yep, that user is signed in. Here's a user object, showing their name and email address, and that they have the roles "Worker", "Potentate", and "Boxer". I have no idea what those roles mean to you, but there they are."
This nicely separates the generic process of defining and assigning roles from the application-specific process of deciding who can do what.
The middleware also simplifies things for the app: it can safely assume that the user is signed in and allowed to use it, because if they weren’t, their request would never even reach it.
Now that the user is authenticated, Authority comes in to authorize user actions.
gatekeeper_client adds a
gatekeeper_user method to
ApplicationController and also calls
helper_method :gatekeeper_client, the application now has a universal way to access info about the single-sign-on user.
To tie things together, we configure Authority with
config.user_method :gatekeeper_user. Now our authorizers can inspect the Gatekeeper user on any request to decide things like “does this user have the required role(s) to delete an Account?”.
Though I said that the host apps had no
User model, did eventually find practical reasons to have one in some apps, for the sake of things like database joins to report on user actions. In those cases, we have a very lightweight model whose attributes are simply kept in sync with the corresponding Gatekeeper user.
Overall, we’ve been very pleased with how easy it is to set up new apps on our domain with authentication and authorization.
We’ve also been pleased with the flexibility Authority has to authorize based on any user object it’s handed, whether that’s an ORM record or a custom-made object. Switching ORMs in one project, while painful overall, was painless as far as Authority was concerned.
Thanks for reading, and be sure to check out Authority if you haven’t already.