Any of them can carry the tenant ID, but none can be trusted by itself. Treat a client-supplied tenant identifier as a selector, then verify the authenticated caller currently belongs to that tenant. A verified token claim is the strongest source, and even then check membership if it can change.
Selector versus proof
The OWASP Multi-Tenant Security Cheat Sheet is direct about this: treat client-supplied tenant identifiers as selectors only, verify that the authenticated principal is authorized to act in the selected tenant, and bind tenant context to a server-verified identity and current membership.
Its bad example is a handler that reads an X-Tenant-ID header and runs a query with it. Parameterized queries stop injection, but they do not stop a caller from naming someone else's tenant.
The three places compared
| Source | Strength | Watch out for |
|---|---|---|
| Signed token claim | Server-verified once the token is validated | Claim can go stale after a membership change |
| Request header | Easy for clients and gateways to set | Fully attacker controlled unless a trusted proxy sets it |
| URL path or subdomain | Visible, cacheable, easy to route | Caller can edit it freely |
A signed claim is safest because the issuer vouched for it. The cheat sheet's own sample code still calls a membership lookup after reading the claim, with a comment that a verified claim may select the tenant but authorization depends on the issuer's guarantees or a current membership check.
A pattern that holds up
- Authenticate the request and validate the token fully, including issuer and audience.
- Read the requested tenant from the claim, or from the path or header if users can belong to several tenants.
- Look up the active membership for the caller and that tenant.
- If there is none, return a denial, not data.
- Store the verified tenant context in request-local state and pass it to every layer that needs it.
- Never let a downstream component replace it with unverified input.
Do this in middleware near the start of the request, so a handler cannot forget it.
Users who belong to several tenants
Consultants and admins often belong to many tenants. A path or header then has to select which one is active. That is fine, because step 3 still checks membership for exactly that pairing. The mistake is assuming that because a user is valid, the tenant they named must be valid.
Keep the selection explicit and short-lived. If a user switches tenant, treat it as a new context and recheck.
Opaque IDs are not the answer
Random identifiers make enumeration harder. The OWASP sheet says opaque, random identifiers can reduce enumeration but are not authorization controls. If an attacker learns a tenant ID from a log, an email or a shared link, the check still has to stop them.
What to test
Log in as a user from tenant A, then send requests with tenant B's ID in the header, the path and the body. Every one must be denied. Repeat after removing the user from a tenant to confirm the membership check is live.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how an endpoint that accepts an identifier from the client gets flagged for review.
Safeguard provides SBOM and dependency scanning plus reachability analysis, which helps you see whether an advisory in your auth libraries touches code you actually call. Try the free scan at gold.safeguard.sh/scan.
Quick check
Is putting the tenant in the subdomain safer?
Not by itself. It is still chosen by the client, so you must check membership for the tenant the host name implies.
Can a gateway set the tenant header for me?
Yes, if it strips any client-supplied copy first and services cannot be reached without it. Otherwise the header is attacker controlled.
Sources
Last reviewed: 19 October 2026.