Our Fellowship team, the Bourbon Planners, built a Jail Population Management Dashboard for the city of Louisville, Ken. This dashboard lets decision makers in Louisville’s criminal justice system visualize and analyze the population in various corrections programs.
For reasons of privacy it became necessary to restrict access to the dashboard to certain people. These people represented various stakeholders in Louisville’s criminal justice system — from judges to corrections officials to public and private attorneys. Some stakeholders were government employees while others were private citizens. Some of the government employees worked for the City of Louisville while others worked for the State of Kentucky.
From a technical standpoint, this meant that there was no single source that could identify all these stakeholders. This made authentication and authorization a challenge but we were able to implement a single, fast, cheap and easy-to-use login method for all stakeholders.
Given the diverse set of stakeholders and the lack of a single identity provider, we briefly flirted with the idea of implementing our own authentication system. While this would certainly solve the single identity provider problem (because our application would play that role) it has a serious downside: correctly implementing a homegrown authentication system is tricky at best. We were dealing with some serious information here – think names of inmates in some cases — and could not risk a security breach. On a less serious note, we also faced the high opportunity cost of implementing a homegrown solution with only a handful of weeks left in the Fellowship.
We considered allowing users to sign-in using their Google, Yahoo, Facebook, or Twitter accounts. We felt each of these options were less than ideal for our application due to several reasons: Each of these services are not just identity providers; they also provide other functionality associated with the identity. Further, our users would have to either re-use their personal identities on one of these services or create a new one and have to remember it just so they could use our application.
After some internal discussion we proposed Mozilla Persona as a solution for authentication. It fit the constraints outlined previously: it is a single source of identity, it is not homegrown, it allows users to reuse their work email addresses as their unique identifiers and, finally, it is no more than simply an identity provider.
While Persona solved our authentication problem (i.e. is this user who she claims to be?), we still needed a way to manage authorization (i.e. is this user allowed to access the Dashboard?). Since we had a relatively small set of users who needed access, we just needed to maintain a whitelist of these users somewhere and check against it when a user signed-in via Persona.
So now the authorization problem was reduced to these questions: where do we store this list of users and their rights and how do we restrict access to this list to certain administrative users?
Once again, we could have chosen to store this list within our application’s realm — either as a flat file or a database table — but that would then require providing the administrators of this list a user interface to manage it. Building this interface and training users on it would require time which, again, was an expensive commodity at that late point in our fellowship.
So instead we chose to store this list as a Google Spreadsheet. This had the obvious benefit of saving us implementation time on the front-end but it also provided administrators with a familiar, Microsoft Excel-like user interface which meant little to no training time. Of course, this meant an integration on the back-end of our application which did take some time and effort. Overall, however, I think we came out net positive and made the right choice.
How does it all work?
When users visit the Dashboard, they are requested to sign-in. When they click the sign-in link, they are taken through the Persona sign-in flow. First-time users — or users with email providers that do not take advantage of Persona’s nifty Identity Bridging feature — are requested to sign up.
Once a user successfully signs in, Persona redirects the user to the Dashboard website with a short-lived assertion token. The Dashboard web server sends this assertion token back to Persona for verification. This makes sure the assertion token was not tampered with on the client side.
Once Persona verifies the assertion token, a session is created on the Dashboard web server for the user to indicate that she is signed in to the Dashboard. Since HTTP is a stateless protocol, this session state is maintained across multiple HTTP requests via a cookie containing the session ID.
When the assertion token is verified by Persona, it responds with the user’s email address. The Dashboard web server uses the Google Drive API to securely retrieve the authorization list from the Google Spreadsheet set up by administrators and checks if the just-signed-in user’s email address exists in this authorization list. If it does not, the user is not authorized for access and an instructive error message is shown to them. If it does and the user has sufficient rights to view the Dashboard (stored alongside the user’s email address in the spreadsheet), the user is shown the Dashboard.
Users with administrative rights are shown additional links to edit the spreadsheet containing the list of authorized users. This spreadsheet is only shared with certain administrative users so even a malicious user who gets a hold of the link to the spreadsheet would not be able to access it.
The back-end API code attempting to authorize the signed-in user’s email address: https://github.com/codeforamerica/in-n-out/blob/master/public/api/v1/user.php#L87
The Authorization class with the ID of the Google Spreadsheet: https://github.com/codeforamerica/in-n-out/blob/master/include/authorization.class.php#L8
Using the Google Drive API to fetch the Google Spreadsheet: https://github.com/codeforamerica/in-n-out/blob/master/include/gdrive_spreadsheet_proxy.class.php#L26-L32
Questions? Comments? Hit us up @codeforamerica.