Software EngineeringBackendAdvanced

Secure Multi-Tenant Cookie-Based JWT Authentication for free

March 19, 2025 5 min read

Secure Multi-Tenant Cookie-Based JWT Authentication for free

REIstacks Authentication System: Secure Multi-Tenant Cookie-Based JWT Authentication

REIstacks utilizes a comprehensive cookie-based JWT authentication system designed specifically for multi-tenant environments. This approach solves the complex challenge of secure authentication across subdomains while maintaining user sessions.

Architecture Overview

The authentication system combines .NET 9 backend API services with Next.js 15 frontend, using Google OAuth for user registration and secure HttpOnly cookies for token storage.

sequenceDiagram participant User participant Next.js Frontend participant Google OAuth participant .NET Backend participant TokenService participant Browser User->>Next.js Frontend: Initiates sign-in Next.js Frontend->>Google OAuth: Redirects to Google Google OAuth->>Next.js Frontend: Returns Google token Next.js Frontend->>.NET Backend: Sends Google token .NET Backend->>TokenService: Verifies token, generates JWT TokenService->>.NET Backend: Returns JWT .NET Backend->>Browser: Sets HttpOnly, Secure Cookies Browser-->>.NET Backend: Sends cookies automatically with future requests

Core Components

Backend (.NET 9)

The backend handles token generation, verification, and refresh logistics:

public class TokenService : ITokenService
{
    public string GenerateAccessToken(UserProfile user, Organization org)
    {
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT_SECRET"]));
        var signingCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Name, user.Name ?? string.Empty),
            new Claim(ClaimTypes.Role, user.Role.ToString())
        };

        // Add organization claims
        if (org != null)
        {
            claims.Add(new Claim("organization_id", org.Id));
            claims.Add(new Claim("subdomain", org.Subdomain));
        }

        var tokenOptions = new JwtSecurityToken(
            issuer: Constants.JwtConstants.Issuer,
            audience: Constants.JwtConstants.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(30),
            signingCredentials: signingCredentials
        );

        return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
    }
}

Frontend (Next.js 15)

The frontend uses a service-based pattern to manage authentication state:

// AuthClientService.ts
export class AuthClientService {
  private static instance: AuthClientService;

  public static getInstance(): AuthClientService {
    if (!AuthClientService.instance) {
      AuthClientService.instance = new AuthClientService();
    }
    return AuthClientService.instance;
  }

  async getUserProfile(): Promise<any> {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/auth/current-user`, {
        method: "GET",
        credentials: 'include'  // Important for sending cookies
      });
      
      if (!res.ok) {
        throw new Error(`Failed to fetch profile: ${res.status}`);
      }
      
      return await res.json();
    } catch (error) {
      console.error("Error fetching user profile:", error);
      throw error;
    }
  }
}

Cookie-Based Authentication

The system uses HttpOnly cookies instead of storing tokens in localStorage, providing enhanced security against XSS attacks:

// Setting cookies in the backend
Response.Cookies.Append("access_token", accessToken, new CookieOptions
{
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.None,
    Domain = ".reistacks.com",
    Expires = DateTime.UtcNow.AddMinutes(30)
});

Multi-Tenant Support

The system supports multiple organizations with subdomains:

// Authentication middleware allowing cross-subdomain auth
app.UseCors(options =>
{
    options.WithOrigins("https://www.reistacks.com", "https://reistacks.com")
          .AllowAnyMethod()
          .AllowAnyHeader()
          .AllowCredentials()
          .SetIsOriginAllowedToAllowWildcardSubdomains();
});

State Management

The frontend uses Zustand for global state management:

// useUserStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { AuthClientService } from '@/services/AuthClientService';

export const useUserStore = create(
  persist(
    (set) => ({
      user: null,
      isAuthenticated: false,
      isLoading: true,

      fetchUser: async () => {
        try {
          const userData = await AuthClientService.getInstance().getUserProfile();
          set({
            user: userData ? {
              id: userData.id,
              name: userData.name,
              role: userData.role,
              // other user properties
            } : null,
            isAuthenticated: !!userData,
            isLoading: false,
          });
        } catch (error) {
          set({ user: null, isAuthenticated: false, isLoading: false });
        }
      },

      logout: async () => {
        try {
          await AuthClientService.getInstance().logout();
        } finally {
          set({ user: null, isAuthenticated: false, isLoading: false });
        }
      },
    }),
    {
      name: 'user-storage',
      storage: createJSONStorage(() => sessionStorage),
    }
  )
);

Context API Integration

React Context organizes access to authentication data:

// AuthContext.tsx
'use client';

import { useUserStore } from '@/stores/useUserStore';
import { createContext, useContext, useEffect } from 'react';

const AuthContext = createContext({
  user: null,
  loading: true,
  logout: () => {},
});

export function AuthProvider({ children }) {
  const { user, isLoading, fetchUser, logout } = useUserStore();

  useEffect(() => {
    if (!user) {
      fetchUser();
    }
  }, [user, fetchUser]);

  return (
    <AuthContext.Provider value={{ user, loading: isLoading, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Route Protection

Next.js middleware handles route protection across subdomains:

// middleware.ts
export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  
  // Skip for public routes
  if (isPublicRoute(pathname)) {
    return NextResponse.next();
  }
  
  // Check auth for protected routes
  if (isProtectedRoute(pathname)) {
    return await checkAuth(request, pathname);
  }
  
  return NextResponse.next();
}

async function checkAuth(request: NextRequest, pathname: string) {
  try {
    const authCheck = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/auth/current-user`, {
      headers: {
        "Cookie": request.headers.get("cookie") || "",
      },
    });

    if (!authCheck.ok) {
      const loginUrl = new URL("/sign-in", request.url);
      loginUrl.searchParams.set("redirect", pathname);
      return NextResponse.redirect(loginUrl);
    }

    return NextResponse.next();
  } catch (error) {
    const loginUrl = new URL("/sign-in", request.url);
    return NextResponse.redirect(loginUrl);
  }
}

Security Features

The system implements multiple security best practices:

  1. HttpOnly Cookies: Tokens are never exposed to JavaScript
  2. Secure Flag: Cookies only transmitted over HTTPS
  3. Domain-Specific Cookies: Supports cross-subdomain authentication
  4. Automatic Token Refresh: Background token refreshing
  5. CSRF Protection: SameSite cookie attributes
  6. OAuth PKCE Flow: Extra protection for OAuth authentication

Usage Example

Using authentication in a component:

'use client';

import { useAuth } from '@/context/AuthContext';

export default function DashboardPage() {
  const { user, loading } = useAuth();
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (!user) {
    return <div>Please sign in to access the dashboard.</div>;
  }
  
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Your role: {user.role}</p>
      {/* Dashboard content */}
    </div>
  );
}

Conclusion

REIstacks implements a robust authentication system that handles the complexities of multi-tenant applications. By combining secure cookie-based JWTs with OAuth authentication and service-based architecture, it provides a secure and seamless user experience while supporting subdomain isolation.

The organization-aware jwt claims and custom domain support make it particularly well-suited for SaaS applications where users need to access their own organization's data across different subdomains while maintaining proper security boundaries.