Why authentication and authorization are the foundation of your SaaS
Every SaaS application has users. And where there are users, you need to answer two fundamental questions: who are you? (authentication) and what are you allowed to do? (authorization). Get this wrong, and your entire product is on shaky ground — from data breaches to angry customers seeing each other's data.
In this deep-dive, we cover the complete stack: from passwords and SSO to role-based access control and tenant isolation. With concrete code examples you can apply right away.
Authentication: getting the basics right
Passwords aren't enough
Yes, you need passwords. But if that's your only authentication method, you're falling behind. Modern SaaS products offer at minimum:
- Email + password with strong hashing (bcrypt or Argon2)
- Magic links via email (passwordless)
- Social login (Google, Microsoft, GitHub)
- Multi-factor authentication (TOTP, SMS, passkeys)
// Password hashing with bcrypt
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Session management with JWT and refresh tokens
Most SaaS applications use a combination of short-lived JWT access tokens and longer-lived refresh tokens:
import jwt from 'jsonwebtoken';
interface TokenPayload {
userId: string;
tenantId: string;
roles: string[];
}
function generateTokens(payload: TokenPayload) {
const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: '15m',
});
const refreshToken = jwt.sign(
{ userId: payload.userId },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
Important: Store refresh tokens in an httpOnly cookie, never in localStorage. This prevents XSS attacks.
Enterprise SSO: SAML and OIDC
Once you start landing larger customers, SSO is a must. They want to connect their own identity provider (Okta, Azure AD, Google Workspace).
There are two standards:
| Protocol | When to use |
|---|---|
| SAML 2.0 | Enterprise customers with existing SAML infrastructure |
| OpenID Connect | More modern setups, easier to implement |
Pro tip: Don't build SSO yourself. Use an auth provider like Auth.js, Clerk, or Auth0. The complexity of SAML XML parsing and certificate rotation isn't worth maintaining yourself.
Authorization: who can do what?
Authentication tells you who someone is. Authorization determines what they're allowed to do. This is where SaaS products often go wrong.
Role-Based Access Control (RBAC)
The most common model. Users get roles, roles have permissions:
// Define your roles and permissions
const PERMISSIONS = {
owner: ['*'], // everything
admin: [
'members:read', 'members:write', 'members:delete',
'billing:read', 'billing:write',
'projects:read', 'projects:write', 'projects:delete',
'settings:read', 'settings:write',
],
member: [
'members:read',
'projects:read', 'projects:write',
],
viewer: [
'members:read',
'projects:read',
],
} as const;
type Role = keyof typeof PERMISSIONS;
function hasPermission(userRoles: Role[], permission: string): boolean {
return userRoles.some(role => {
const perms = PERMISSIONS[role];
return perms.includes('*') || perms.includes(permission);
});
}
// Middleware example
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!hasPermission(req.user.roles, permission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete('/api/projects/:id',
requirePermission('projects:delete'),
deleteProjectHandler
);
When RBAC isn't enough: ABAC
Sometimes you need finer-grained control. Attribute-Based Access Control (ABAC) looks at properties of the user, the resource, and the context:
interface AccessContext {
user: { id: string; role: Role; department: string };
resource: { type: string; ownerId: string; tenantId: string };
action: string;
environment: { time: Date; ipAddress: string };
}
function evaluatePolicy(ctx: AccessContext): boolean {
// Users can only edit their own resources
if (ctx.action === 'edit' && ctx.resource.ownerId !== ctx.user.id) {
// Unless they're an admin
if (ctx.user.role !== 'admin') return false;
}
// Sensitive actions only during business hours
if (ctx.action === 'delete' && ctx.resource.type === 'financial') {
const hour = ctx.environment.time.getHours();
if (hour < 8 || hour > 18) return false;
}
return true;
}
Multi-tenant isolation: the holy grail
In a multi-tenant SaaS, it is absolutely crucial that tenant A can never access tenant B's data. You need to enforce this at multiple layers:
1. Database level: Row-Level Security
PostgreSQL offers Row-Level Security (RLS), a powerful mechanism:
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Create a policy
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- In your application, set the tenant for each request
SET LOCAL app.current_tenant_id = 'tenant-uuid-here';
2. Application level: middleware
// Tenant middleware - runs on every request
async function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const tenantId = req.user?.tenantId;
if (!tenantId) {
return res.status(401).json({ error: 'No tenant context' });
}
// Set tenant context for all database queries
req.tenantScope = {
where: { tenantId },
};
// With Prisma: use middleware or extensions
req.prisma = prisma.$extends({
query: {
$allModels: {
async $allOperations({ args, query }) {
args.where = { ...args.where, tenantId };
return query(args);
},
},
},
});
next();
}
3. API level: validation
Every API call must validate that the requested resource belongs to the correct tenant:
async function getProject(req: Request, res: Response) {
const project = await req.prisma.project.findUnique({
where: { id: req.params.id },
});
// Double check: does this resource belong to the tenant?
if (!project || project.tenantId !== req.user.tenantId) {
return res.status(404).json({ error: 'Not found' });
}
res.json(project);
}
Golden rule: Always return a 404 (not 403) when a user requests a resource from another tenant. A 403 confirms the resource exists, which is an information leak.
Common mistakes
1. Authorization only in the frontend
// ❌ WRONG: just hiding the button
{user.role === 'admin' && <DeleteButton />}
// ✅ RIGHT: also check server-side
app.delete('/api/resource/:id', requireRole('admin'), handler);
Frontend checks are UX, not security. Anyone can send an API request.
2. No audit logging
Every authorization decision should be logged, especially for sensitive actions:
async function auditLog(event: {
userId: string;
tenantId: string;
action: string;
resource: string;
result: 'allowed' | 'denied';
metadata?: Record<string, unknown>;
}) {
await db.auditLog.create({ data: { ...event, timestamp: new Date() } });
}
3. Overly broad API tokens
Always give API keys the minimum scope needed. Implement scoped tokens:
interface ApiKey {
key: string;
tenantId: string;
scopes: string[]; // ['projects:read', 'webhooks:write']
expiresAt: Date;
rateLimit: number;
}
Checklist for your SaaS
Before going live, check these points:
- Passwords hashed with bcrypt/Argon2 (never MD5/SHA)
- MFA available for all users
- JWT access tokens with short lifespan (< 30 min)
- Refresh tokens in httpOnly cookies
- RBAC with server-side enforcement
- Tenant isolation at database level (RLS or query scoping)
- Audit logging for sensitive actions
- Rate limiting on auth endpoints
- Brute-force protection (account lockout)
- CORS properly configured
- CSP headers set
Conclusion
Authentication and authorization aren't features you bolt on later — they're the foundations of your SaaS. Start with a solid base (a reliable auth provider + RBAC), add tenant isolation from day one, and expand to SSO and fine-grained permissions as your customers grow.
The most important principle: defense in depth. Don't rely on a single layer. Combine database isolation, middleware checks, API validation, and audit logging. That's how you build a SaaS your customers can trust.