Single-tenant authorisation asks what this person may do. Multi-tenant authorisation asks what this person may do here, and the second question does not decompose into the first. The same consultant is an owner in their own workspace, an editor at one client and a read-only guest at another, all at the same time, in the same browser.
Every difficult case in this area is a variation on that sentence. Get the attachment point right and they are all easy; get it wrong and each one arrives as an incident.
The role belongs to the membership
A membership is the object that joins a person to an organisation, and it is where the role lives. Not on the user, which cannot vary by context, and not on the organisation, which cannot vary by person. This is a small amount of indirection that removes an entire category of bug, and it is nearly free to add before you have data.
It follows that the session must name an organisation. A session that says who but not where forces every authorisation check to derive context from the request, which means a URL parameter, which means a missing check somewhere is a cross-tenant read. Switching organisation issues a new session rather than mutating the existing one, so the change is atomic and appears in the audit trail.
- Role on the membership; the user record carries identity, not authority
- Organisation in the session, resolved once, never recomputed from the path
- Switching organisation mints a new session and logs the switch
- Every query carries the tenant predicate structurally, not by convention
A session that says who but not where pushes tenant context into the URL, and the URL is attacker-supplied.
The cases that break naive models
Invitations create a person who has a role before they have an account, which means the membership must be able to exist in a pending state without a user attached. Nested teams create a person whose effective permissions are the union of several grants, and unions need a defined answer for conflicts — deny wins is the only rule that stays comprehensible.
Then there is your own staff. Support needs to see a customer's workspace to help them, and the tempting implementation is a flag on the user record that bypasses the tenant predicate. That flag will eventually be granted too widely, will not appear in the customer's audit log, and is exactly what an enterprise reviewer asks about. Model internal access as a time-boxed, logged, customer-visible membership like any other.
Who may change a role
The last piece is the one that gets designed by accident. Role assignment is itself a permission, and it needs a rule about privilege escalation: a member cannot grant a role they do not hold, and cannot remove the last owner of an organisation. Both are three lines of code and both are humiliating to add after somebody demonstrates the alternative.
Where roles come from a directory, decide precedence explicitly and say so in the interface. If a group grants editor and an administrator set viewer by hand, one of them wins at the next sync, and the customer deserves to know which before they discover it.
- No granting a role above your own; no removing the final owner
- Directory-derived roles and manual overrides need a stated precedence
- Show where a permission came from — group, role or direct grant
- Role changes are audit events with before and after, not just a new value
Everything here, already built
Sign-in, enterprise SSO, directory provisioning, roles and an audit trail behind one API. Start with the quickstart and have a working sign-in this afternoon.