Back to blog
authenticationauthorizationSaaSsecuritymulti-tenantRBAC

Authentication and Authorization in Your SaaS: The Complete Guide to Secure Access

By SaaS Masters11 maart 20267 min read
Authentication and Authorization in Your SaaS: The Complete Guide to Secure Access

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:

ProtocolWhen to use
SAML 2.0Enterprise customers with existing SAML infrastructure
OpenID ConnectMore 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.