"Security is not a feature. It's a requirement."
Verifikasi Identitas
Kontrol Akses
Defense in Depth
Di Bab 8, Task Manager bekerja — tetapi semua orang melihat data yang sama. user_id masih hardcoded. Tidak ada privacy, tidak ada security.
Data accessible oleh siapa saja. user_id di-hardcode di code.
Real users, sessions, access control, dan defense-in-depth security.
Production environment dengan HTTPS dan monitoring.
Semua data visible oleh siapa saja yang buka aplikasi.
user_id hardcoded — tidak ada konsep "milik saya" vs "milik orang lain".
RLS policies tidak berfungsi karena auth.uid() selalu NULL.
Tidak ada audit trail — tidak tahu siapa melakukan apa.
"Siapa Anda?"
Verifikasi identitas user. Login dengan email + password, OAuth, biometric, dll.
Analogi: Menunjukkan ID card di pintu masuk gedung.
"Apa yang boleh Anda lakukan?"
Kontrol akses berdasarkan permissions. User A boleh edit, User B read-only.
Analogi: Access card menentukan ruangan mana yang boleh Anda masuki.
Authentication tanpa Authorization = pintu terbuka untuk semua orang yang punya ID. Authorization tanpa Authentication = tidak tahu siapa yang punya akses.
// JWT Structure: header.payload.signature eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJ1c2VyX2lkIjoiMTIzNDUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MTE1MzYwMDB9. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Algorithm (HS256, RS256) dan type (JWT).
User data: ID, email, roles, expiration time.
Hash dari header + payload + secret key. Prevent tampering.
⚠️ JWT payload is BASE64 encoded, NOT encrypted. Jangan taruh sensitive data (password, credit card).
auth.users table dengan user profilesTidak perlu build auth dari nol. Supabase handle complexity, Anda fokus di business logic.
import { supabase } from '@/lib/supabase';
async function handleSignup(email, password) {
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
options: {
emailRedirectTo: `${window.location.origin}/auth/confirm`
}
});
if (error) {
console.error('Signup error:', error.message);
return;
}
// Supabase sends verification email
alert('Check your email for verification link');
}
async function handleLogin(email, password) {
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password
});
if (error) {
console.error('Login error:', error.message);
return;
}
// data.user contains user info
// data.session contains JWT token
console.log('Logged in as:', data.user.email);
router.push('/dashboard');
}
Token tersimpan di localStorage (default) atau bisa customized ke httpOnly cookie.
Supabase client auto-refresh expired tokens sebelum expire.
// LOGOUT
async function handleLogout() {
const { error } = await supabase.auth.signOut();
if (error) console.error('Logout error:', error.message);
else router.push('/login');
}
// PASSWORD RESET
async function handlePasswordReset(email) {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`
});
if (error) console.error('Reset error:', error.message);
else alert('Check your email for reset link');
}
signOut() menghapus token dari storage dan invalidate sessionimport { useEffect, useState } from 'react';
function useAuth() {
const [user, setUser] = useState(null);
useEffect(() => {
// Get current user
supabase.auth.getUser().then(({ data }) => {
setUser(data.user);
});
// Listen to auth state changes
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
}
);
return () => authListener.subscription.unsubscribe();
}, []);
return user;
}
✓ Sekarang Anda punya user.id untuk replace hardcoded user_id di Bab 8!
// middleware.js (Next.js App Router)
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
export async function middleware(req) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req, res });
const { data: { session } } = await supabase.auth.getSession();
// Redirect ke login jika tidak authenticated
if (!session) {
return NextResponse.redirect(new URL('/login', req.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
};
-- Di Bab 8, auth.uid() return NULL -- Sekarang, setelah user login, auth.uid() return user ID CREATE POLICY "Users can view their own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id); -- ✓ Sekarang bekerja! CREATE POLICY "Users can insert their own tasks" ON tasks FOR INSERT WITH CHECK (auth.uid() = user_id);
Moment ini adalah di mana security layer yang Anda bangun di Bab 8 "hidup." RLS policies yang tadinya dormant sekarang aktif dan enforce data isolation.
// ❌ Bab 8: Hardcoded
const { data } = await supabase
.from('tasks')
.insert({
title: 'New task',
user_id: '12345' // BAD
});
// ✅ Bab 9: Dynamic
const user = await supabase.auth.getUser();
const { data } = await supabase
.from('tasks')
.insert({
title: 'New task',
user_id: user.data.user.id
});
Create, read, update own tasks. Tidak bisa lihat tasks orang lain.
Full access ke semua tasks. Bisa delete any task, manage users.
Read all tasks, moderate content, tapi tidak bisa delete users.
Role stored di auth.users metadata atau separate user_roles table.
-- Admin bisa lihat semua tasks CREATE POLICY "Admins can view all tasks" ON tasks FOR SELECT USING ( (SELECT role FROM auth.users WHERE id = auth.uid()) = 'admin' ); -- Regular user hanya lihat miliknya CREATE POLICY "Users can view own tasks" ON tasks FOR SELECT USING ( auth.uid() = user_id OR (SELECT role FROM auth.users WHERE id = auth.uid()) = 'admin' );
Subquery di RLS bisa lambat. Consider denormalize role ke separate table dengan index.
| Storage | Security | Use Case |
|---|---|---|
| localStorage | ⚠️ Vulnerable to XSS | SPAs, client-heavy |
| httpOnly Cookie | ✅ XSS-safe | Server-rendered, high security |
Supabase default: access token expires 1 hour, refresh token expires 30 days. Customizable via dashboard.
// ❌ VULNERABLE: User input langsung di-render
<div>{userInput}</div>
// User submit: <script>alert(document.cookie)</script>
// Result: Script dijalankan, cookie stolen!
// ✅ SAFE: React auto-escapes
<div>{userInput}</div> // React mengubah < jadi <
// ❌ BYPASS: dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // NEVER DO THIS
Attacker inject malicious script via input field, comment, atau URL parameter.
Sanitize input, escape output, Content Security Policy (CSP) headers.
// ❌ VULNERABLE: Accept any POST request
app.post('/api/delete-account', (req, res) => {
deleteAccount(req.user.id); // No CSRF protection
});
// Attacker site bisa trigger:
<form action="https://yourapp.com/api/delete-account" method="POST">
<input type="submit" value="Click for prize!" />
</form>
// ✅ PROTECTED: Verify CSRF token
app.post('/api/delete-account', verifyCsrfToken, (req, res) => {
deleteAccount(req.user.id);
});
// ❌ VULNERABLE: Tidak verify ownership
app.get('/api/tasks/:id', async (req, res) => {
const task = await db.tasks.findById(req.params.id);
res.json(task); // Siapa saja bisa akses task ID manapun!
});
// ✅ PROTECTED: Verify user owns resource
app.get('/api/tasks/:id', async (req, res) => {
const task = await db.tasks.findOne({
id: req.params.id,
user_id: req.user.id // ✓ Check ownership
});
if (!task) return res.status(403).json({ error: 'Forbidden' });
res.json(task);
});
RLS di Supabase automatically prevent IDOR — database enforce ownership check. Tapi tetap validate di application layer!
// ❌ VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
// User input: ' OR '1'='1
// Result: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns ALL users!
// ✅ SAFE: Parameterized queries
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userInput); // Supabase auto-escape
Data leak, data manipulation, authentication bypass, bahkan DROP DATABASE.
ALWAYS use parameterized queries atau ORM. NEVER concat user input into SQL.
Allow "password123" atau tidak enforce complexity. Enforce min 12 chars + complexity.
Unlimited login attempts → brute force possible. Limit 5 attempts/15 min.
Sequential session IDs atau weak random. Use cryptographically secure random.
Session ID tidak regenerate setelah login. Always regenerate.
Hardcoded secrets di code atau commit ke git. Use env vars.
Session tidak invalidated saat logout. Properly destroy session.
Jangan rely pada single security layer. Stack multiple layers sehingga jika satu layer fail, layer lain masih protect.
// next.config.js
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'geolocation=(), microphone=()' },
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline';"
}
]
}];
}
};
default-src 'self' — hanya load dari own domainscript-src 'self' — block inline scriptsstyle-src 'self' — block inline stylesimg-src * — allow images dari anywhereCSP strict bisa break third-party scripts (Google Analytics, ads). Balance security vs functionality.
Start dengan CSP dalam report-only mode untuk detect violations tanpa block. Iteratively tighten policy.
Data transmitted in clear text. Passwords, tokens, semua visible ke attacker di network.
TLS encryption. Man-in-the-middle attacker hanya lihat gibberish.
Supabase auto-provides HTTPS endpoints. Next.js Vercel deployments auto-HTTPS. Localhost dev dapat exception (HTTP OK).
// Supabase built-in rate limiting di auth endpoints
// Config via dashboard: Settings → API → Rate Limits
// Custom rate limiting dengan middleware
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Max 5 attempts
message: 'Too many login attempts. Try again later.'
});
app.post('/login', loginLimiter, handleLogin);
5 attempts / 15 min per IP
100 requests / minute per user
3 requests / hour per email
AI optimize untuk "code yang bekerja," bukan "code yang aman." Security trade-offs tidak visible di AI training data.
⚠️ NEVER blindly copy-paste AI-generated auth/security code. ALWAYS review dengan Security Checklist.
## Security Review: Login API ### Authentication: ✅ - Password hashed dengan bcrypt (cost factor 12) - Rate limiting: 5 attempts / 15 min per IP - Session token: JWT dengan 1-hour expiration ### Input Validation: ⚠️ - Email format validated - **MISSING:** Password strength check - **RECOMMENDATION:** Enforce min 12 chars + complexity ### Transport Security: ✅ - HTTPS enforced via middleware - httpOnly cookies untuk session storage ### Error Handling: ❌ - Error message: "Invalid email or password" ✓ - **ISSUE:** Server error leak stack trace ke client - **FIX:** Sanitize error messages di production ### Keputusan: REQUEST CHANGES (fix error leak + password policy)
<script>alert(1)</script>Think like an attacker. Try to break your own app. Better you find bugs than real attackers.
IDOR, privilege escalation, missing authorization.
Weak encryption, plaintext passwords, exposed secrets.
SQL, NoSQL, OS command, LDAP injection.
Missing rate limiting, inadequate threat modeling.
Default credentials, verbose errors, missing headers.
Outdated libraries dengan known vulnerabilities.
Full list: owasp.org/Top10
// Audit log table
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
action TEXT NOT NULL, -- 'login', 'create_task', 'delete_user'
resource_type TEXT, -- 'task', 'user'
resource_id BIGINT,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
// Log setiap sensitive action
async function logAction(userId, action, resourceType, resourceId) {
await supabase.from('audit_logs').insert({
user_id: userId,
action: action,
resource_type: resourceType,
resource_id: resourceId,
ip_address: req.ip,
user_agent: req.headers['user-agent']
});
}
Monitoring & alerts untuk unusual activity.
Isolate affected systems. Revoke compromised credentials.
Analyze logs. Determine scope & root cause.
Patch vulnerability. Update affected users.
Restore normal operations. Monitor untuk re-attack.
Post-mortem. Update security policies.
Have incident response plan BEFORE breach happens. Practice drills. Know who to contact.
// config.js export const DB_PASSWORD = 'super_secret_123'; export const JWT_SECRET = 'my-jwt-key'; // .env file committed ke git DATABASE_URL=postgres://user:pass@...
// .env.local (gitignored) DATABASE_URL=postgres://... JWT_SECRET=... // .gitignore .env.local .env.*.local
// Check vulnerabilities npm audit // Auto-fix jika available npm audit fix // View dependency tree npm ls // Keep dependencies updated npm outdated npm update
Malicious code injected ke popular packages. Review package before install. Use lock files.
Snyk, Dependabot, GitHub Security Alerts — auto-detect vulnerable dependencies dan create PRs.
// ❌ Too permissive
Access-Control-Allow-Origin: * // Any domain bisa akses API
// ✅ Specific origins
Access-Control-Allow-Origin: https://yourapp.com
// Next.js API route
export default function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', 'https://yourapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight
if (req.method === 'OPTIONS') return res.status(200).end();
// ... rest of handler
}
6-digit code via text message. Simple tapi vulnerable ke SIM swapping.
Google Authenticator, Authy. Time-based codes. More secure.
YubiKey, USB security keys. Most secure, phishing-resistant.
2FA significantly reduce account takeover risk. Even if password leaked, attacker tetap butuh second factor.
Supabase support 2FA via phone verification. Implementasi di Bab 10 (Advanced Topics).
Protect data saat transmission dari browser ke server. Prevent man-in-the-middle attacks.
Encrypt data di database. Jika server compromised, attacker tidak bisa read data.
Add "Login with Google" atau GitHub via Supabase Auth providers.
Passwordless login via email link (seperti Notion, Slack).
Add SMS atau TOTP second factor dengan Supabase phone auth.
Build admin panel dengan RBAC. View all users, manage permissions.
UI untuk view audit logs. Filter by user, action, date range.
Run OWASP ZAP scan terhadap app. Document findings dan fixes.
Deployment & Production — HTTPS setup, environment variables, CI/CD, monitoring, dan production security hardening.
Multi-tenancy, API rate limiting tiers, advanced RBAC, compliance (GDPR, SOC2).
"Aplikasi yang 'bekerja' belum tentu siap production. Security lapisan demi lapisan adalah yang membedakan demo dari aplikasi nyata."
Anda sekarang memiliki security-first mindset. Aplikasi Anda bukan hanya functional — it's secure.
Real users, sessions, JWT tokens
RLS, RBAC, data isolation
Defense in depth, vulnerability prevention
Next: Deploy aplikasi production-ready Anda ke web di Bab 10.