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
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
- Token expiry setting phù hợp?
- Có cần global sign out?
- Vấn đề gì với session management?