Bỏ qua

Session Management

Session trong Supabase

Session Structure

┌─────────────────────────────────────────────────────────────┐
│                    SUPABASE SESSION                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  {                                                           │
│    access_token: "eyJhbGc...",   // JWT, 1 hour default     │
│    refresh_token: "abc123...",   // Long-lived              │
│    expires_at: 1234567890,       // Unix timestamp          │
│    expires_in: 3600,             // Seconds                 │
│    token_type: "bearer",                                    │
│    user: {                                                  │
│      id: "uuid",                                            │
│      email: "user@example.com",                             │
│      ...                                                    │
│    }                                                        │
│  }                                                           │
│                                                              │
└─────────────────────────────────────────────────────────────┘

JWT Token Explained

Access Token Structure

// Decode JWT (don't verify, just read)
const [header, payload, signature] = accessToken.split('.');

// Payload contains:
const decoded = JSON.parse(atob(payload));
// {
//   aud: "authenticated",
//   exp: 1234567890,           // Expiration
//   sub: "user-uuid",          // User ID
//   email: "user@example.com",
//   role: "authenticated",
//   app_metadata: {...},
//   user_metadata: {...}
// }

Token Lifespan

┌─────────────────────────────────────────────────────────────┐
│                    TOKEN LIFECYCLE                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Access Token                                               │
│  ├── Lifespan: 1 hour (default, configurable)              │
│  ├── Used for: API requests                                │
│  └── Refresh: Automatic with refresh_token                 │
│                                                              │
│  Refresh Token                                              │
│  ├── Lifespan: Configurable (default: 1 week)              │
│  ├── Used for: Getting new access_token                    │
│  └── One-time use: Rotated on each refresh                 │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Get Current Session

Basic Session Check

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

if (session) {
  console.log('Logged in as:', session.user.email);
  console.log('Token expires:', new Date(session.expires_at! * 1000));
} else {
  console.log('Not logged in');
}

Get User Data

// Get user (validates token with server)
const { data: { user }, error } = await supabase.auth.getUser();

if (user) {
  console.log('User ID:', user.id);
  console.log('Email:', user.email);
  console.log('Metadata:', user.user_metadata);
}

getSession vs getUser

// getSession() - Reads from local storage (fast, no network)
// - Use for quick checks
// - Token might be expired/invalid
const { data: { session } } = await supabase.auth.getSession();

// getUser() - Validates with server (slower, network call)
// - Use when you need verified user data
// - Ensures token is valid
const { data: { user } } = await supabase.auth.getUser();

Session Events

Listen for Auth Changes

// Subscribe to auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    console.log('Auth event:', event);
    console.log('Session:', session);

    switch (event) {
      case 'SIGNED_IN':
        // User just signed in
        break;
      case 'SIGNED_OUT':
        // User signed out
        break;
      case 'TOKEN_REFRESHED':
        // Token was refreshed
        break;
      case 'USER_UPDATED':
        // User data was updated
        break;
      case 'PASSWORD_RECOVERY':
        // Password reset flow
        break;
    }
  }
);

// Cleanup
subscription.unsubscribe();

React Context Pattern

Auth Provider

'use client';

import { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '@supabase/supabase-js';
import { createClient } from '@/lib/supabase/client';

type AuthContext = {
  user: User | null;
  session: Session | null;
  loading: boolean;
};

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

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [session, setSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(true);

  const supabase = createClient();

  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });

    // Listen for changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );

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

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

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

Using Auth Context

// In components
function Dashboard() {
  const { user, loading } = useAuth();

  if (loading) return <Loading />;
  if (!user) return <Redirect to="/login" />;

  return <div>Welcome, {user.email}</div>;
}

// Protected route wrapper
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [user, loading]);

  if (loading) return <Loading />;
  if (!user) return null;

  return children;
}

Token Refresh

Auto Refresh (Default)

// Supabase client auto-refreshes tokens
// Happens automatically when:
// 1. Making API request with expired token
// 2. onAuthStateChange fires TOKEN_REFRESHED

// No manual action needed!

Manual Refresh

// Force refresh token
const { data: { session }, error } = await supabase.auth.refreshSession();

if (error) {
  console.error('Refresh failed:', error.message);
  // Redirect to login
} else {
  console.log('New token:', session?.access_token);
}

Handle Refresh Errors

supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'TOKEN_REFRESHED') {
    console.log('Token refreshed successfully');
  }

  // If session is null and we expected one
  if (!session && event !== 'SIGNED_OUT') {
    // Refresh token expired/invalid
    // Redirect to login
    window.location.href = '/login';
  }
});

Sign Out

Basic Sign Out

const { error } = await supabase.auth.signOut();

if (error) {
  console.error('Sign out error:', error.message);
} else {
  // Session cleared
  // Redirect to home
}

Sign Out Everywhere

// Sign out from all devices/sessions
const { error } = await supabase.auth.signOut({
  scope: 'global'
});

Next.js Sign Out

'use client';

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

function SignOutButton() {
  const router = useRouter();
  const supabase = createClient();

  const handleSignOut = async () => {
    await supabase.auth.signOut();
    router.push('/');
    router.refresh(); // Clear server-side cache
  };

  return <button onClick={handleSignOut}>Sign Out</button>;
}

Session in Server Components

Server-side Session Check

// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const supabase = await createClient();

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

  if (!user) {
    redirect('/login');
  }

  return <Dashboard user={user} />;
}

Middleware Protection

// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

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

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

  // Protect /dashboard routes
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return response;
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

Session Settings

Configure in Dashboard

Dashboard → Authentication → Settings

JWT Expiry: 3600 (seconds) - 1 hour default
Refresh Token Rotation: Enabled (recommended)
Refresh Token Reuse Interval: 10 seconds

Security Best Practices

✅ Enable refresh token rotation
✅ Short access token expiry (1 hour)
✅ Use secure cookies (HTTPS only)
✅ Clear session on sign out
✅ Handle token refresh errors
✅ Validate user server-side for sensitive operations

Tổng kết

Session Methods

Method Purpose
getSession() Get cached session
getUser() Get verified user
refreshSession() Force token refresh
signOut() End session
onAuthStateChange() Listen for changes

Token Types

Token Lifespan Purpose
Access 1 hour API requests
Refresh 1 week Get new access token

Q&A

  1. Token expiry setting phù hợp?
  2. Có cần global sign out?
  3. Vấn đề gì với session management?