Building Secure Multi-Tenant Authentication with js-cookie and React

Multi-tenant SaaS applications are revolutionizing how businesses deliver software, allowing multiple customers (tenants) to share a single platform while keeping their data and sessions isolated. Whether you're building an admin portal or an employee portal, secure authentication is critical to ensure each tenant's experience is private and seamless.

In this Blog, I'll walk through creating a robust multi-tenant authentication system using "js-cookie "for secure session management and React for a dynamic, tenant-aware frontend. Leveraging a monorepo setup with tools like eslint-config-custom and Turbo scripts ( turbo run lint ), we'll build a scalable solution for SaaS platforms.

Why js-cookie and React?

  • js-cookie: Lightweight library for managing cookies securely with options like secure , sameSite , and subdomain sharing.

  • React: Ideal for dynamic UIs, using hooks and context to manage tenant-based authentication state.

  • Custom Auth Module: A reusable package in your monorepo for tenant-specific logic.

  • 2025 Security Trends: Emphasize strict cookie policies, subdomain sharing, and server-side validation for multi-tenant SaaS apps.

This combination delivers fast, secure, and scalable authentication for multi-tenant environments.

Prerequisites

Ensure your package.json includes:

  • "js-cookie": "^3.0.5" and "@types/js-cookie": "^3.0.6"

  • "eslint": "^8.22.0" and "eslint-config-custom": "workspace:*"

  • A monorepo with packages like admin-portal , operator-portal , and auth .

Install dependencies

  
    npm install js-cookie @types/js-cookie eslint react-router-dom
  

Project structure

  
    project/
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ admin-portal/
β”‚   β”œβ”€β”€ auth/
β”‚   └── component-library/
β”œβ”€β”€ src/
β”‚   └── auth.js
  

Step 1. Tenant-Based Authentication Module

Create packages/auth/src/index.js to handle tenant-specific sessions:

  
    import Cookies from 'js-cookie';

// Auth service for tenant-based sessions
export const AuthService = {
  login: async (tenantId, userCredentials) => {
    try {
      const response = await fetch(`/api/auth/${tenantId}/login`, {
        method: 'POST',
        body: JSON.stringify(userCredentials),
        headers: { 'Content-Type': 'application/json' },
      });
      if (!response.ok) throw new Error('Login failed');
      const { token } = await response.json();

      Cookies.set(`auth_token_${tenantId}`, token, {
        expires: 1,
        secure: true,
        sameSite: 'Strict',
        path: '/',
      });

      return token;
    } catch (error) {
      throw new Error(`Login error: ${error.message}`);
    }
  },

  getToken: (tenantId) => Cookies.get(`auth_token_${tenantId}`),
  logout: (tenantId) => Cookies.remove(`auth_token_${tenantId}`, { path: '/', secure: true }),
  isAuthenticated: (tenantId) => !!Cookies.get(`auth_token_${tenantId}`),
};
  

Security Highlights

  • Tenant Isolation with unique cookie keys.

  • Secure cookies ( secure , sameSite ).

  • Optional subdomain sharing via domain .

  • Clear error handling.

Step 2. Integrating with React

Use React Context in packages/admin-portal/src/App.jsx :

  
import React, { createContext, useState, useEffect, useContext } from 'react';
import { AuthService } from '../auth';
import { useNavigate } from 'react-router-dom';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [tenantId, setTenantId] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const navigate = useNavigate();

  useEffect(() => {
    const hostname = window.location.hostname;
    const tenant = hostname.includes('.') ? hostname.split('.')[0] : 'default';
    setTenantId(tenant);
    setIsAuthenticated(AuthService.isAuthenticated(tenant));
  }, []);

  const login = async (credentials) => {
    try {
      await AuthService.login(tenantId, credentials);
      setIsAuthenticated(true);
      navigate('/dashboard');
    } catch (error) {
      alert('Login failed');
    }
  };

  const logout = () => {
    AuthService.logout(tenantId);
    setIsAuthenticated(false);
    navigate('/login');
  };

  return (
    <AuthContext.Provider value={{ tenantId, isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

const LoginPage = () => {
  const { login, tenantId } = useContext(AuthContext);
  const handleSubmit = (e) => {
    e.preventDefault();
    const credentials = { username: e.target.username.value, password: e.target.password.value };
    login(credentials);
  };

  return (
    <div style={{ maxWidth: '400px', margin: '50px auto' }}>
      <h2>Login for Tenant: {tenantId}</h2>
      <form onSubmit={handleSubmit}>
        <input type="text" name="username" placeholder="Username" />
        <input type="password" name="password" placeholder="Password" />
        <button type="submit">Login</button>
      </form>
    </div>
  );
};
  

Step 3. Securing with ESLint

Add security-focused linting:

  
    module.exports = {
  rules: {
    'no-eval': 'error',
    'no-console': ['warn', { allow: ['error'] }],
    'security/detect-object-injection': 'error',
    'no-unused-vars': ['error', { vars: 'all', args: 'none' }],
  },
  plugins: ['security'],
  env: { browser: true, node: true },
};

turbo run lint
  

Step 4. Optimization and Security Best Practices

  • Use HttpOnly cookies and server-side JWT validation.

  • Validate tenantId on the backend.

  • Cache tenant configs for performance.

  • Share AuthService across portals in the monorepo.

  • Enable subdomain cookie sharing when needed.

  • Queue login events with @azure/service-bus .

  • Use luxon for timezone-aware logging.

Real-World Use Case

Imagine a SaaS platform where companies manage their teams via isolated dashboards. A tenant logs in at tenant1.yourapp.com, and their session is stored securely with js-cookie . The system scales to thousands of tenants worldwide, handling spikes and logging accurately across regions.

Conclusion

With js-cookie and React, you can craft a secure, multi-tenant authentication system that’s both developer-friendly and production-ready. By combining a custom auth module, enforcing security with ESLint, and optimizing with Turbo’s workflows, you’ll ensure scalability and safety for SaaS apps.