Sitemap
Tharushi Dilara

Tharushi Dilara

The “Multi-Tenant” Rule: How to Safely Architect Authentication for SaaS

6 min read6 days ago

--

Press enter or click to view image in full size

When you build a standard application, authentication is simple. A user logs in, you verify their password, and you give them a session. Done.

But when you are building a B2B SaaS platform (like an ERP or Inventory system), you face a terrifying architectural challenge: **Multi-Tenancy**.

If Company A and Company B both use your software, how do you guarantee with 100% certainty that Company A can never see Company B’s inventory, suppliers, or financial transactions?

During the development of a multi tenant inventory system, I learned that security isn’t just about hiding buttons on the frontend. It is about a defense-in-depth architecture that starts at the login screen and ends at the database query.

Here is the blueprint for building safe, multi-tenant authentication.

Press enter or click to view image in full size

1. The Core Concept: The “Tenant” is not the “User”

The biggest mistake developers make is treating the `User` as the tenant.

In a SaaS system, the User is just an identity. The Organization (or Company/Tenant) is the actual data container.
* User A works for Company X.
* User B works for Company Y.
* User C also works for Company X.

When a user logs in, your system must not only ask ”Who are you?” but also ”Which organization are you acting on behalf of?”

2. The JWT Payload: Embedding the Tenant Identity

When a user successfully authenticates via your login route, you generate a JSON Web Token (JWT).

Most developers put the `userId` and `role` in the payload. For a multi-tenant app, you must include the `organizationId`.

If you are using an external Auth Provider (like Auth0, AWS Cognito, or Clerk), you inject this via Custom Claims. Because custom claims can collide with standard JWT claims, they are usually namespaced.

```json
{
“sub”: “user_12345”,
“https://company.com/organizationId": “org_98765”,
“https://company.com/role": “COMPANY_ADMIN”
}
```

Now, the `organizationId` is cryptographically signed and travels with the user on every request. The frontend cannot tamper with it to view another company’s data.

3. Enter JWKS: The Enterprise Standard (HS256 vs. RS256)

If you are building a simple monolith, you might use a shared `JWT_SECRET` string (Symmetric Encryption, HS256) to sign and verify tokens.

But for a true SaaS microservice architecture, this is a massive security risk.** If your Inventory Service gets compromised, the attacker gets the secret string and can forge tokens for the entire system.

Instead, you should use Asymmetric Encryption (RS256) and JWKS (JSON Web Key Set).
* Your Auth Server holds a Private Key to sign tokens.
* Your Inventory Service only holds the Public Key to verify them.

Press enter or click to view image in full size

4. The Middleware: Your API Bouncer

Every time the frontend makes an API request, it sends this JWT in the headers. You need an authentication middleware that verifies the token using the JWKS endpoint, and extracts the `organizationId`.

Get Tharushi Dilara’s stories in your inbox

Join Medium for free to get updates from this writer.

Here is how you implement this securely in Node.js using the `jwks-rsa` package:

```javascript
import jwt from ‘jsonwebtoken’;
import jwksClient from ‘jwks-rsa’;

// 1. Setup the JWKS Client (Point to your Auth Provider)

const client = jwksClient({
jwksUri: ‘https://auth.company.com/.well-known/jwks.json',
cache: true, // Cache keys to prevent fetching on every request
rateLimit: true,
});

// 2. Function to retrieve the correct signing key

function getKey(header, callback) {
client.getSigningKey(header.kid, function (err, key) {
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}

3. The Multi-Tenant Middleware

export const authenticateJWKS = (req, res, next) => {
const token = req.headers.authorization?.split(“ “)[1];
if (!token) return res.status(401).json({ error: “Unauthorized” });

jwt.verify(token, getKey, { algorithms: [‘RS256’] }, (err, decoded) => {
if (err) return res.status(401).json({ error: “Invalid token” });

// Extract namespaced custom claims
const namespace = “https://comapny.com/";
const organizationId = decoded[`${namespace}organizationId`];
const role = decoded[`${namespace}role`];

if (!organizationId) {
return res.status(403).json({ error: “No tenant context found in token” });
}

// Attach the tenant context to the request object
req.tenant = {
userId: decoded.sub,
organizationId: organizationId,
role: role
};

next();
});
};
```

5. The Database Enforcer: The Final Line of Defense

This is where systems often fail. You have the `organizationId` in the request, but if your database query is just `prisma.material.findMany()`, you will return *every* material for *every* company in the database.

Rule #1 of Multi-Tenancy: Every single query must filter by `organizationId`.

In our architecture, every table (`Material`, `Supplier`, `InventoryBatch`, `PurchaseOrder`) has an `organizationId` column. The backend controller must use the tenant context from the middleware to scope the query.

```javascript
// Inside your material controller
export const getMaterials = async (req, res) => {
const { organizationId } = req.tenant; // Extracted from JWT

// The database physically filters out other tenants’ data
const materials = await prisma.material.findMany({
where: {
organizationId: organizationId,
isActive: true
}
});

return res.json(materials);
};
```

If a malicious user from Company A tries to access a specific material from Company B by guessing the URL (`GET /api/materials/12345`), your backend must do this:

```javascript
const material = await prisma.material.findFirst({
where: {
id: req.params.id,
organizationId: req.tenant.organizationId // ← The enforcer
}
});

if (!material) return res.status(404).json({ error: “Not found” });
// We return 404 instead of 403 so hackers don’t even know the record exists.
```

Press enter or click to view image in full size

6. Composite Unique Keys: Preventing Collisions

In a multi-tenant system, two different companies might want to use the exact same SKU or Material Code (e.g., `MAT-0001`).

If you put a global `@unique` constraint on `materialCode`, Company B’s database insert will crash because Company A already used that code.

To fix this, use **Composite Unique Keys** in Prisma:
```prisma
model Material {
id String @id @default(uuid())
organizationId String
materialCode String

@@unique([organizationId, materialCode])
}
```
This tells the database: *”It’s okay if `MAT-0001` exists multiple times, as long as each one belongs to a different `organizationId`.”*

Press enter or click to view image in full size

7. Role-Based Access Control (RBAC) within the Tenant

Multi-tenancy isn’t just about isolating companies; it’s also about isolating users within a company.

In ERP, we can have two main roles:
COMPANY_ADMIN: Can create Suppliers, delete Materials, and perform Stock Adjustments.
COMPANY_USER: Can only view stock and perform Stock In/Out.

Your middleware must handle this before the request hits the database:

```javascript
export const requireAdmin = (req, res, next) => {
if (req.tenant.role !== “COMPANY_ADMIN”) {
return res.status(403).json({ error: “Admin access required” });
}
next();
};

// Usage in routes
router.post(“/adjustments”, authenticateJWKS, requireAdmin, adjustmentController);
```

Press enter or click to view image in full size

Summary
Securing a SaaS application requires a layered approach:
1. Identity: JWKS verifies the token cryptographically. Custom claims hold the `organizationId`.
2. Transport: Middleware extracts the tenant context on every request.
3. Persistence: Every Prisma query enforces the `organizationId` filter.
4. Database: Composite unique keys prevent cross-tenant data collisions.

If you implement these four layers, you can sleep soundly knowing that your database is a fortress. One missed `where: { organizationId }` clause could be the difference between a successful SaaS product and a catastrophic data breach.

Are you building a multi-tenant SaaS? What is your strategy for keeping tenant data isolated? Let me know in the comments!

--

--