DEV Community

Cover image for Advanced Authentication Patterns with Next.js and Supabase
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on • Originally published at iloveblogs.blog

Advanced Authentication Patterns with Next.js and Supabase

Advanced Authentication Patterns with Next.js and Supabase

Authentication is the gateway to your application. While basic email/password authentication works for simple apps, production applications need sophisticated authentication patterns that balance security, user experience, and business requirements. This guide teaches you advanced authentication techniques used by leading SaaS companies.

Authentication Fundamentals

Before diving into advanced patterns, understand these core concepts:

Authentication vs Authorization:

  • Authentication: Verifies who you are (login)
  • Authorization: Determines what you can do (permissions)

Stateful vs Stateless:

  • Stateful: Server stores session data (traditional approach)
  • Stateless: Client stores token, server verifies (JWT approach)

Token Types:

  • Access Token: Short-lived (1 hour), used for API requests
  • Refresh Token: Long-lived (7 days), used to get new access tokens
  • ID Token: Contains user information, used for authentication

1. OAuth 2.0 Implementation

OAuth delegates authentication to trusted providers, reducing your security burden.

Configuring OAuth Providers

In Supabase Dashboard:

  1. Go to Authentication → Providers
  2. Enable desired providers (Google, GitHub, Discord, etc.)
  3. Add OAuth credentials from provider
  4. Configure redirect URLs

Implementing OAuth Sign-In

// app/auth/oauth/page.tsx
'use client';

import { createClient } from '@/lib/supabase/client';
import { useRouter } from 'next/navigation';

export default function OAuthPage() {
  const router = useRouter();
  const supabase = createClient();

  async function signInWithGoogle() {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`
      }
    });

    if (error) {
      console.error('OAuth error:', error);
    }
  }

  async function signInWithGitHub() {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'github',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`
      }
    });

    if (error) {
      console.error('OAuth error:', error);
    }
  }

  return (
    <div className="space-y-4">
      <button onClick={signInWithGoogle} className="btn btn-google">
        Sign in with Google
      </button>
      <button onClick={signInWithGitHub} className="btn btn-github">
        Sign in with GitHub
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

OAuth Callback Handler

// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const error = searchParams.get('error');

  if (error) {
    return NextResponse.redirect(
      new URL(`/auth/error?error=${error}`, request.url)
    );
  }

  if (code) {
    const cookieStore = cookies();
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          get(name: string) {
            return cookieStore.get(name)?.value;
          },
          set(name: string, value: string, options) {
            cookieStore.set({ name, value, ...options });
          },
          remove(name: string, options) {
            cookieStore.set({ name, value: '', ...options });
          },
        },
      }
    );

    const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);

    if (!exchangeError) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return NextResponse.redirect(new URL('/auth/error', request.url));
}
Enter fullscreen mode Exit fullscreen mode

Account Linking

Allow users to link multiple OAuth providers:

// Link additional OAuth provider to existing account
async function linkOAuthProvider(provider: string) {
  const supabase = createClient();

  const { data, error } = await supabase.auth.linkIdentity({
    provider,
    options: {
      redirectTo: `${window.location.origin}/auth/callback`
    }
  });

  if (error) {
    console.error('Linking error:', error);
  }
}

// Unlink OAuth provider
async function unlinkOAuthProvider(provider: string) {
  const supabase = createClient();

  const { data, error } = await supabase.auth.unlinkIdentity({
    identity_id: 'provider_id'
  });

  if (error) {
    console.error('Unlinking error:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Passwordless Authentication

Passwordless auth improves security and user experience by eliminating passwords.

Magic Link Authentication

// app/auth/magic-link/page.tsx
'use client';

import { createClient } from '@/lib/supabase/client';
import { useState } from 'react';

export default function MagicLinkPage() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');
  const supabase = createClient();

  async function handleMagicLink(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);

    const { error } = await supabase.auth.signInWithOtp({
      email,
      options: {
        emailRedirectTo: `${window.location.origin}/auth/callback`
      }
    });

    if (error) {
      setMessage(`Error: ${error.message}`);
    } else {
      setMessage('Check your email for the magic link!');
      setEmail('');
    }

    setLoading(false);
  }

  return (
    <form onSubmit={handleMagicLink} className="space-y-4">
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Sending...' : 'Send Magic Link'}
      </button>
      {message && <p>{message}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

One-Time Password (OTP)

// Send OTP via SMS or email
async function sendOTP(phone: string) {
  const supabase = createClient();

  const { data, error } = await supabase.auth.signInWithOtp({
    phone,
    options: {
      shouldCreateUser: true
    }
  });

  if (error) {
    console.error('OTP error:', error);
  }
}

// Verify OTP
async function verifyOTP(phone: string, token: string) {
  const supabase = createClient();

  const { data, error } = await supabase.auth.verifyOtp({
    phone,
    token,
    type: 'sms'
  });

  if (error) {
    console.error('Verification error:', error);
  }

  return data;
}
Enter fullscreen mode Exit fullscreen mode

3. Custom JWT Implementation

For advanced use cases, implement custom JWT claims:

// lib/jwt.ts
import jwt from 'jsonwebtoken';

interface CustomClaims {
  sub: string; // user id
  email: string;
  organization_id: string;
  role: 'admin' | 'editor' | 'viewer';
  permissions: string[];
  iat: number;
  exp: number;
}

export function createCustomJWT(user: {
  id: string;
  email: string;
  organization_id: string;
  role: string;
  permissions: string[];
}): string {
  const claims: CustomClaims = {
    sub: user.id,
    email: user.email,
    organization_id: user.organization_id,
    role: user.role as any,
    permissions: user.permissions,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
  };

  return jwt.sign(claims, process.env.JWT_SECRET!);
}

export function verifyCustomJWT(token: string): CustomClaims | null {
  try {
    return jwt.verify(token, process.env.JWT_SECRET!) as CustomClaims;
  } catch (error) {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Custom Claims in Middleware

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyCustomJWT } from '@/lib/jwt';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  const claims = verifyCustomJWT(token);

  if (!claims) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Check authorization
  if (request.nextUrl.pathname.startsWith('/admin') && claims.role !== 'admin') {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  // Add claims to request headers
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', claims.sub);
  requestHeaders.set('x-organization-id', claims.organization_id);
  requestHeaders.set('x-user-role', claims.role);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*', '/api/:path*']
};
Enter fullscreen mode Exit fullscreen mode

4. Multi-Tenant Authentication

Implement authentication for multi-tenant SaaS applications:

// lib/multi-tenant-auth.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function getUserWithOrganization() {
  const cookieStore = cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options) {
          cookieStore.set({ name, value, ...options });
        },
        remove(name: string, options) {
          cookieStore.set({ name, value: '', ...options });
        },
      },
    }
  );

  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return null;
  }

  // Get user's organizations
  const { data: organizations } = await supabase
    .from('organization_members')
    .select(`
      organization_id,
      role,
      organizations(id, name, slug)
    `)
    .eq('user_id', user.id);

  return {
    user,
    organizations: organizations || []
  };
}

// Verify user belongs to organization
export async function verifyOrganizationAccess(
  userId: string,
  organizationId: string
): Promise<boolean> {
  const cookieStore = cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options) {
          cookieStore.set({ name, value, ...options });
        },
        remove(name: string, options) {
          cookieStore.set({ name, value: '', ...options });
        },
      },
    }
  );

  const { data } = await supabase
    .from('organization_members')
    .select('id')
    .eq('user_id', userId)
    .eq('organization_id', organizationId)
    .single();

  return !!data;
}
Enter fullscreen mode Exit fullscreen mode

Multi-Tenant Middleware

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyOrganizationAccess } from '@/lib/multi-tenant-auth';

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

  // Extract organization slug from URL
  const match = pathname.match(/^\/org\/([^/]+)/);
  if (!match) {
    return NextResponse.next();
  }

  const organizationSlug = match[1];
  const userId = request.headers.get('x-user-id');

  if (!userId) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Verify user has access to organization
  const hasAccess = await verifyOrganizationAccess(userId, organizationSlug);

  if (!hasAccess) {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/org/:path*']
};
Enter fullscreen mode Exit fullscreen mode

5. Enterprise SSO (SAML)

For enterprise customers, implement SAML-based SSO:

// Configure SAML provider
async function configureSAML(organizationId: string, samlConfig: {
  entityId: string;
  ssoUrl: string;
  certificate: string;
}) {
  const supabase = createClient();

  const { data, error } = await supabase
    .from('organization_sso')
    .insert({
      organization_id: organizationId,
      provider: 'saml',
      config: samlConfig
    });

  if (error) {
    console.error('SAML config error:', error);
  }

  return data;
}

// SAML sign-in
async function signInWithSAML(organizationId: string) {
  const supabase = createClient();

  // Get SAML config
  const { data: ssoConfig } = await supabase
    .from('organization_sso')
    .select('*')
    .eq('organization_id', organizationId)
    .eq('provider', 'saml')
    .single();

  if (!ssoConfig) {
    throw new Error('SAML not configured for this organization');
  }

  // Redirect to SAML provider
  window.location.href = ssoConfig.config.ssoUrl;
}
Enter fullscreen mode Exit fullscreen mode

6. Step-Up Authentication

Require additional verification for sensitive operations:

// lib/step-up-auth.ts
const STEP_UP_TIMEOUT = 15 * 60 * 1000; // 15 minutes

export async function requireStepUpAuth(userId: string): Promise<boolean> {
  const lastStepUp = localStorage.getItem(`step-up-${userId}`);
  const now = Date.now();

  if (!lastStepUp || now - parseInt(lastStepUp) > STEP_UP_TIMEOUT) {
    // Require re-authentication
    return false;
  }

  return true;
}

export function recordStepUpAuth(userId: string) {
  localStorage.setItem(`step-up-${userId}`, Date.now().toString());
}

// Usage in sensitive operation
async function changePassword(userId: string, newPassword: string) {
  const hasStepUp = await requireStepUpAuth(userId);

  if (!hasStepUp) {
    // Redirect to re-authentication
    throw new Error('Step-up authentication required');
  }

  // Change password
  const supabase = createClient();
  const { error } = await supabase.auth.updateUser({
    password: newPassword
  });

  if (!error) {
    recordStepUpAuth(userId);
  }

  return error;
}
Enter fullscreen mode Exit fullscreen mode

7. Authentication Error Handling

// lib/auth-errors.ts
export function getAuthErrorMessage(error: any): string {
  const errorCode = error?.code || error?.message;

  const messages: Record<string, string> = {
    'invalid_credentials': 'Invalid email or password',
    'user_not_found': 'User not found',
    'email_not_confirmed': 'Please confirm your email',
    'weak_password': 'Password is too weak',
    'user_already_exists': 'User already exists',
    'over_request_rate_limit': 'Too many requests. Please try again later',
    'session_not_found': 'Session expired. Please log in again',
    'invalid_grant': 'Invalid credentials',
    'invalid_request': 'Invalid request'
  };

  return messages[errorCode] || 'An authentication error occurred';
}

// Usage
async function handleLogin(email: string, password: string) {
  const supabase = createClient();

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

  if (error) {
    const message = getAuthErrorMessage(error);
    console.error(message);
    return { error: message };
  }

  return { success: true };
}
Enter fullscreen mode Exit fullscreen mode

8. Authentication Best Practices Checklist

  • ✅ Use HTTPS in production
  • ✅ Store tokens in httpOnly cookies
  • ✅ Implement token refresh logic
  • ✅ Use strong password requirements
  • ✅ Implement rate limiting on auth endpoints
  • ✅ Enable MFA for sensitive accounts
  • ✅ Log authentication events
  • ✅ Implement step-up authentication for sensitive operations
  • ✅ Use OAuth for consumer apps
  • ✅ Implement SAML for enterprise customers
  • ✅ Handle authentication errors gracefully
  • ✅ Implement account linking
  • ✅ Regular security audits

Related Articles

Conclusion

Advanced authentication patterns enable you to build secure, scalable applications that meet diverse user and business requirements. Start with basic OAuth for consumer apps, add passwordless authentication for better UX, and implement enterprise features like SAML for B2B customers.

Remember: authentication is the foundation of security. Invest time in getting it right, and your users will thank you with their trust.


Originally published at https://iloveblogs.blog

Top comments (0)