Secure Multi-Tenant Cookie-Based JWT Authentication for free
March 19, 2025 • 5 min read

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.
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:
- HttpOnly Cookies: Tokens are never exposed to JavaScript
- Secure Flag: Cookies only transmitted over HTTPS
- Domain-Specific Cookies: Supports cross-subdomain authentication
- Automatic Token Refresh: Background token refreshing
- CSRF Protection: SameSite cookie attributes
- 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.