Multitenancy and Authentication
Luddy Harrison, July 26, 2023
Multitenant architectures
We are so accustomed to the idea that a cloud back end serves multiple clients, that it seems almost strange to call this capability out as a special case. Nevertheless, arranging for a back end to serve multiple users, while protecting their privacy from one another, offering good service to all of them, and generally keeping everything straight, is real work and not something that can be taken for granted. It is such a profound consideration, that it is fundamental to the architecture of the back end. It must be factored into nearly every aspect of the back end design.
multitenant architecture
A multitenant architecture is one in which a single instance of the architecture serves multiple users
This stands in contrast to an architecture that is made to serve multiple users by instantiating it separately for each user. There are many cloud services that support one customer by spinning up a dedicated server and running the service on this server. Wordpress is a good example. Basic Wordpress hosting entails spinning up a server and deploying Wordpress on it. When authoring with Wordpress (as opposed to using it as a website hosting platform), the server is basically serving one user or a group of closely related users who are building a website. This isn't a multitenant architecture, but there are many such Wordpress servers running, serving different users.
An application like Gmail is perhaps the ultimate multitenant architecture. Gmail serves untold numbers of users. Privacy of each user's email is paramount. Instantiating the Gmail infrastructure individually for each user is a non-starter.
Databases and multitenancy
In a multitenant application, each user has his own view of the state of the application — his profile, data, history, etc. — and his state is largely independent from the state of every other user of the application. In fact, by default, it should be impossible for one user to know anything at all about another user's application state. This is a paramount privacy consideration, and when it is broken, headlines are made. There are of course situations in which users communicate or share data with one another, but these are carefully controlled and deliberate.
Because of its primacy, we need a dead-simple and essentially foolproof method for separating one user's database records from other users'. Thinking about this problem for just a moment, the first thing that occurs to us is that if we are to keep users' data separate, we must begin by having a unique identifier for each user, a sort of vocabulary of user identities. We must moreover trust this identity more or less absolutely, because if user privacy is to be built on this identity as a foundation, then any weakness in its trustworthiness will call into question the integrity of the whole solution.
The second thing that occurs to us is that we need a way to make use of these user identities that isolates users' database records from one another in an airtight way.
multitenant databases
The two foundations for multitenant databases are trustworthy user identities, and a partition scheme that makes use of these identities to isolate each user's database records from those of other users.
Authentication
Our multitenant application has many users, and these users communicate with the back end, first and foremost, using HTTP requests. See the earlier article in this series on APIs. The first order of business, then, is to determine which user is making each HTTP request to the back end. The HTTP request by itself doesn't tell us this, certainly not in a way we can rely on. To solve this problem, multitenant applications introduce a system of sign up and sign in — authentication, in a word.
Sign Up
The first step in establishing user identities is to require would-be users of the application to sign up for an account. During the sign-up process, the user presents some form of identity, like an email address or phone number, that can be verified. Typically, a code is sent to the email address or phone number, and the user is asked to enter it back into the sign-up dialog. This establishes that this user has access to that email address or phone number. That is typically taken as good enough proof of idenitity.1
The sign-up process creates a new user identity. That identity is in a 1:1 relationship with the email or phone number that was verified. It might be a unique hash, UUID, unique customer number, or whatever. The form it takes isn't especially important.
The sign up process is handled by an authentication server. The new user identity is registered with this server and is ready for use in authenticating this user to the application.
Sign In
The next step is for the user to sign in to the application. This is some variation on the now-familiar login screen that protects nearly every cloud-based application these days. The user gives identity in some form or another — the email, phone number, or a unique user name — and a password. There might be other factors of authentication, like a one-time code generated by an authentication app running on the user's device. This login information is sent to the authentication server, which has registered the identity of user in the sign up process.
This is where the first bit of magic happens. When the sign-in is accepted — the password and other factors are verified — the authentication server sends back to the client (e.g., the user's browser) a set of tokens that certify the identity of the user, and that are cryptographically signed by the authentication server.
The client attaches these tokens to every HTTP request to the back end. The back end verifies the signature of the tokens, i.e., establishes that it can trust the information they contain, and then reads the user identity from them. It now has associated a trustworthy user identity with the HTTP request it has received.
As you can imagine, every manner of attack is possible against these tokens. If a third party gets ahold of them, they can pretend to be the authenticated user. But the tokens are sent back and forth over secure connections, and as with verification of email or phone number, the back end takes the basic security infrastructure for granted and focuses on its own role in enforcing and maintaining security.
User Identities as Database Keys
We want, so far as possible, to make it impossible for the back end to inadvertently access one user's data while intending to access another's. The simplest method for accomplishing this is to use the user identity to create a database key that can only be associated with that user.
S3 Prefixes
As an example, let's consider the AWS workhorse key-value store, S3. In S3, objects are stored in buckets. The name of the object is its key, and the contents of the object is its value. By convention, a folder-like structure can be imposed on objects to keep them organized. This is done by attaching prefixes to the object names, for example A/
or B/
. We know that the object named A/foo
is distinct from the object B/foo
just by looking at their prefixes A/
and B/
; it is impossible for their names to refer to the same object.
Since user identities are unique -- we rely on the authentication service and the tokens it generates for this -- then prefixing an object name with the identity of the user it belongs to is sufficient to safely and simply keep users' objects separate. For example, if we have two users whose identities are 00001234
and 00001235
, then by naming the objects belonging to these users 00001234/...
and 00001235/...
, we have made it impossible for the well-formed name of an object belonging to the first user to accidentally name an object belonging to the second user.
DynamoDB Partition and Sort Keys
AWS DynamoDB is a NoSQL database that supports ismple or compound keys. For our purpose here, we can suppose that a simple key is a string and a compound key is two strings.
A simple key is a single value, for example a single string. A compound key is two valu In the simplest scheme, a item in the database is identified by a single key, called the partition key.
Multitenancy and Performance
-
a book could be written on the nuances and vulnerabilities of various forms of idenitity verification, and we have all read stories of compromised email accounts, spoofed SIM cards, and the like. But the cloud back isn't and can't be responsible for all that. Its starting point must be a form of identity that is taken as verified. The concern of the back end is handling that idenity faithfully and enforcing it to protect users' privacy. ↩