projectrules.ai

Authentication Patterns

AuthenticationSecuritySupabaseRole-based Access ControlSession Management

Description

Guidelines for data-auth

Globs

**/*
---
description: Guidelines for data-auth
globs: **/*
---

# Authentication Patterns

## AI Guidelines
Implement secure authentication using Supabase Auth with proper session management, role-based access control, and tenant isolation. Create middleware for route protection, validate user identity in all actions, and follow secure token handling practices. Ensure consistent error handling for authentication failures.

## Key Patterns

### Authentication Flow
- **Supabase Auth**: Use Supabase for all authentication
- **Email/Password**: Primary authentication method
- **Social Auth**: Optional OAuth providers (Google, GitHub)
- **Magic Link**: Passwordless email authentication
- **Multi-tenant**: User associated with specific tenant
- **Role Assignment**: Role-based access control

### Session Management
- **Token Storage**: Secure cookie-based token storage
- **Session Refresh**: Automatic token refresh mechanism
- **Session Validation**: Middleware validation on all protected routes
- **Session Termination**: Proper logout handling

### User Context
- **Authentication Check**: `getUser()` helper for server actions
- **User Provider**: Client-side user context provider
- **Permission Helpers**: Role-based permission checking

### Tenant Isolation
- **Tenant Identifier**: Use `tenant_id` in all user operations
- **Row-Level Security**: Supabase RLS policies for tenant isolation
- **Default Tenant**: First-time login tenant assignment
- **Cross-Tenant Prevention**: Block cross-tenant access attempts

## Examples

### Authentication Implementation

```typescript
// In /src/lib/supabase/auth.ts
import { createClient } from '@/lib/supabase/supabase-server';
import { cookies } from 'next/headers';

export async function signIn(email: string, password: string): Promise<DbResponse> {
  try {
    const supabase = createClient(cookies());

    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password
    });

    if (error) {
      return { success: false, error: error.message };
    }

    // Fetch user profile with tenant and role info
    const { data: profile, error: profileError } = await supabase
      .from('profiles')
      .select('*')
      .eq('id', data.user.id)
      .single();

    if (profileError) {
      return { success: false, error: 'Failed to fetch user profile' };
    }

    return {
      success: true,
      data: {
        id: data.user.id,
        email: data.user.email!,
        name: profile.name,
        tenant_id: profile.tenant_id,
        role: profile.role
      }
    };
  } catch (error) {
    console.error('Error in signIn:', error);
    return { success: false, error: 'Authentication failed' };
  }
}

export async function signOut(): Promise<DbResponse> {
  try {
    const supabase = createClient(cookies());
    const { error } = await supabase.auth.signOut();

    if (error) {
      return { success: false, error: error.message };
    }

    return { success: true };
  } catch (error) {
    console.error('Error in signOut:', error);
    return { success: false, error: 'Failed to sign out' };
  }
}

export async function getCurrentUser(): Promise<DbResponse> {
  try {
    const supabase = createClient(cookies());

    const { data: { session }, error: sessionError } = await supabase.auth.getSession();

    if (sessionError || !session) {
      return { success: false, error: 'No active session' };
    }

    const { data: profile, error: profileError } = await supabase
      .from('profiles')
      .select('*')
      .eq('id', session.user.id)
      .single();

    if (profileError) {
      return { success: false, error: 'Failed to fetch user profile' };
    }

    return {
      success: true,
      data: {
        id: session.user.id,
        email: session.user.email!,
        name: profile.name,
        tenant_id: profile.tenant_id,
        role: profile.role
      }
    };
  } catch (error) {
    console.error('Error in getCurrentUser:', error);
    return { success: false, error: 'Failed to get current user' };
  }
}
```

### Server-Side Authentication Action

```typescript
// In /src/app/actions/user.ts
'use server';

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/lib/supabase/auth';
import { serverCache } from '@/lib/cache';

export async function getUser(): Promise<ActionResult> {
  try {
    // Check cache first (short TTL for security)
    const cacheKey = 'current_user';
    const cachedUser = serverCache.get(cacheKey);

    if (cachedUser) {
      return { success: true, data: cachedUser };
    }

    // Get user from Supabase
    const result = await getCurrentUser();

    if (!result.success) {
      return { success: false, error: 'Authentication required' };
    }

    // Cache user data (short TTL)
    serverCache.set(cacheKey, result.data, { ttl: 60 }); // 60 seconds

    return { success: true, data: result.data };
  } catch (error) {
    console.error('Error in getUser:', error);
    return { success: false, error: 'Failed to authenticate user' };
  }
}

export async function requireAuth(redirectTo: string = '/login') {
  const userResult = await getUser();

  if (!userResult.success) {
    // If client-side, return data for client-side redirect
    if (typeof window !== 'undefined') {
      return { authenticated: false, redirectUrl: redirectTo };
    }

    // If server-side, redirect immediately
    redirect(redirectTo);
  }

  return { authenticated: true, user: userResult.data };
}

export async function requireRole(
  requiredRoles: string[],
  redirectTo: string = '/unauthorized'
) {
  const { authenticated, user, redirectUrl } = await requireAuth();

  if (!authenticated) {
    return { authorized: false, redirectUrl };
  }

  if (!requiredRoles.includes(user.role)) {
    // If client-side, return data for client-side redirect
    if (typeof window !== 'undefined') {
      return { authorized: false, redirectUrl: redirectTo };
    }

    // If server-side, redirect immediately
    redirect(redirectTo);
  }

  return { authorized: true, user };
}
```

### Authentication Middleware

```typescript
// In /src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/supabase-server';

export async function middleware(request: NextRequest) {
  // Get the pathname of the request
  const { pathname } = request.nextUrl;

  // Skip authentication check for public routes
  if (
    pathname.startsWith('/_next') ||
    pathname.startsWith('/api/auth') ||
    pathname === '/login' ||
    pathname === '/register' ||
    pathname === '/forgot-password'
  ) {
    return NextResponse.next();
  }

  // Create supabase server client
  const supabase = createClient();

  // Check if user is authenticated
  const { data: { session }, error } = await supabase.auth.getSession();

  // If no session or error, redirect to login
  if (error || !session) {
    const url = new URL('/login', request.url);
    url.searchParams.set('redirect', pathname);
    return NextResponse.redirect(url);
  }

  // For tenant-specific paths, verify tenant access
  if (pathname.includes('/[tenant]/')) {
    // Extract tenant from URL (based on your URL structure)
    const tenantMatch = pathname.match(/\/([^\/]+)\/dashboard/);
    if (tenantMatch && tenantMatch[1]) {
      const requestedTenant = tenantMatch[1];

      // Fetch user's tenant
      const { data: profile, error: profileError } = await supabase
        .from('profiles')
        .select('tenant_id')
        .eq('id', session.user.id)
        .single();

      // If error or tenant mismatch, redirect to unauthorized
      if (profileError || profile.tenant_id !== requestedTenant) {
        return NextResponse.redirect(new URL('/unauthorized', request.url));
      }
    }
  }

  // Continue with the request
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};
```

### Client-Side Authentication

```typescript
// In /src/context/auth/AuthProvider.tsx
'use client';

import { createContext, useContext, useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/supabase-browser';
import { useRouter } from 'next/navigation';

interface AuthContextValue {
  user: AuthUser | null;
  loading: boolean;
  signIn: (email: string, password: string) => Promise<ActionResult>;
  signOut: () => Promise;
  isRole: (role: string | string[]) => boolean;
}

const AuthContext = createContext(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const router = useRouter();
  const supabase = createClient();

  useEffect(() => {
    // Fetch initial session
    const initAuth = async () => {
      setLoading(true);

      try {
        const { data: { session } } = await supabase.auth.getSession();

        if (session) {
          // Fetch user profile
          const { data } = await supabase
            .from('profiles')
            .select('*')
            .eq('id', session.user.id)
            .single();

          setUser({
            id: session.user.id,
            email: session.user.email!,
            name: data.name,
            tenant_id: data.tenant_id,
            role: data.role
          });
        }
      } catch (error) {
        console.error('Error initializing auth:', error);
      } finally {
        setLoading(false);
      }
    };

    initAuth();

    // Set up auth state change listener
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        if (event === 'SIGNED_IN' && session) {
          // Fetch user profile
          const { data } = await supabase
            .from('profiles')
            .select('*')
            .eq('id', session.user.id)
            .single();

          setUser({
            id: session.user.id,
            email: session.user.email!,
            name: data.name,
            tenant_id: data.tenant_id,
            role: data.role
          });
        } else if (event === 'SIGNED_OUT') {
          setUser(null);
          router.push('/login');
        }
      }
    );

    return () => {
      subscription.unsubscribe();
    };
  }, [supabase, router]);

  const signIn = async (email: string, password: string) => {
    try {
      const { data, error } = await supabase.auth.signInWithPassword({
        email,
        password
      });

      if (error) {
        return { success: false, error: error.message };
      }

      // Fetch user profile
      const { data: profile, error: profileError } = await supabase
        .from('profiles')
        .select('*')
        .eq('id', data.user.id)
        .single();

      if (profileError) {
        return { success: false, error: 'Failed to fetch user profile' };
      }

      const user = {
        id: data.user.id,
        email: data.user.email!,
        name: profile.name,
        tenant_id: profile.tenant_id,
        role: profile.role
      };

      setUser(user);
      return { success: true, data: user };
    } catch (error) {
      console.error('Error signing in:', error);
      return { success: false, error: 'Authentication failed' };
    }
  };

  const signOut = async () => {
    await supabase.auth.signOut();
    setUser(null);
    router.push('/login');
  };

  const isRole = (roleOrRoles: string | string[]) => {
    if (!user) return false;

    const roles = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles];
    return roles.includes(user.role);
  };

  return (
    
      {children}
    
  );
}

export function useAuth() {
  const context = useContext(AuthContext);

  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }

  return context;
}
```

## Security Best Practices

1. **Token Management**
   - Store tokens only in HTTPOnly, Secure cookies
   - Never store tokens in localStorage
   - Implement short token expiration times
   - Use refresh tokens for longer sessions

2. **Password Security**
   - Enforce strong password policies
   - Implement rate limiting for login attempts
   - Use Supabase's password hashing (Argon2)
   - Support two-factor authentication

3. **Multi-Tenancy**
   - Associate users with specific tenants
   - Use RLS policies based on tenant_id
   - Validate tenant access in middleware
   - Prevent tenant ID manipulation

4. **Permission Management**
   - Implement granular RBAC permissions
   - Check permissions in all actions
   - Keep permission checks consistent
   - Log access violations

## Related Rules
- core-architecture.mdc - Three-layer architecture
- api-design.mdc - API design patterns
- data-supabase.mdc - Database access patterns
- api-implementation.mdc - API implementation
- ui-state.mdc - State management
Authentication Patterns