Memuat...
👋 Selamat Pagi!

Cara Membangun Authentication System yang Aman Tanpa Library Pihak Ketiga

Pelajari cara membuat sistem autentikasi sendiri dari nol tanpa bergantung pada library pihak ketiga. Panduan lengkap untuk developer yang ingin kontrol penuh a...

Cara Membangun Authentication System yang Aman Tanpa Library Pihak Ketiga

Menggunakan library authentication seperti Passport, Auth0, atau Laravel Sanctum memang praktis dan cepat.

Tapi pernahkah Anda bertanya kenapa sebagian developer senior lebih memilih membangun authentication system sendiri dari nol?

Jawabannya sederhana: kontrol penuh, pemahaman mendalam, dan fleksibilitas tanpa batas.

Di artikel ini, kita akan membedah cara membangun sistem autentikasi yang aman tanpa bergantung pada library pihak ketiga.

Anda akan belajar konsep fundamental yang sering diabaikan, implementasi praktis, dan teknik keamanan yang wajib diterapkan.

Mengapa Membangun Authentication System Sendiri?

Sebelum masuk ke implementasi teknis, mari pahami dulu kenapa skill ini penting.

Pertama, Anda akan memahami setiap detail proses autentikasi dari hashing password hingga session management.

Kedua, tidak ada ketergantungan pada library yang mungkin tidak di-maintain lagi atau memiliki vulnerability tersembunyi.

Ketiga, Anda bisa menyesuaikan 100% dengan kebutuhan bisnis tanpa terbatas fitur bawaan library.

Terakhir, ini adalah skill fundamental yang membedakan junior dan senior developer.

Prinsip Dasar Keamanan Authentication

Ada beberapa prinsip yang harus Anda pegang sebelum menulis satu baris kode pun.

Never trust user input. Validasi setiap input dari user tanpa kecuali.

Never store plain text passwords. Selalu gunakan hashing algorithm yang proven seperti bcrypt atau Argon2.

Use HTTPS only. Jangan pernah transmit credentials melalui HTTP biasa.

Implement rate limiting. Cegah brute force attacks dengan membatasi login attempts.

Use secure session management. Regenerate session ID setelah login dan logout.

Arsitektur Authentication System yang Robust

Sistem autentikasi yang baik terdiri dari beberapa komponen utama yang bekerja bersama.

Komponen pertama adalah User Registration dengan validasi input dan password hashing.

Komponen kedua adalah Login Mechanism yang verify credentials dan create session.

Komponen ketiga adalah Session Management untuk maintain user state across requests.

Komponen keempat adalah Password Reset dengan token-based verification.

Dan komponen terakhir adalah Security Layer untuk CSRF protection, rate limiting, dan audit logging.

Implementasi Password Hashing yang Benar

Mari mulai dengan fondasi paling krusial: password hashing.

Jangan pernah gunakan MD5 atau SHA1 untuk password. Algoritma ini sudah tidak aman.

<?php
// SALAH - Jangan pernah lakukan ini
$hashedPassword = md5($password);
$hashedPassword = sha1($password);

// BENAR - Gunakan bcrypt dengan cost factor yang sesuai
function hashPassword($password) {
    $options = [
        'cost' => 12, // Adjust based on server capability
    ];
    return password_hash($password, PASSWORD_BCRYPT, $options);
}

// Verify password
function verifyPassword($password, $hash) {
    return password_verify($password, $hash);
}
?>

Cost factor 12 adalah sweet spot antara keamanan dan performa untuk kebanyakan aplikasi.

Jika server Anda powerful, Anda bisa naikkan ke 13 atau 14.

Membangun User Registration yang Aman

Registration adalah pintu masuk pertama, jadi harus super ketat.

Validasi setiap field dengan teliti sebelum menyimpan ke database.

<?php
class UserRegistration {
    private $db;
    
    public function register($email, $password, $confirmPassword) {
        // Validate email format
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception("Email tidak valid");
        }
        
        // Check email uniqueness
        if ($this->emailExists($email)) {
            throw new Exception("Email sudah terdaftar");
        }
        
        // Validate password strength
        if (!$this->isStrongPassword($password)) {
            throw new Exception("Password terlalu lemah");
        }
        
        // Verify password confirmation
        if ($password !== $confirmPassword) {
            throw new Exception("Password tidak cocok");
        }
        
        // Hash password
        $hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
        
        // Generate verification token
        $verificationToken = bin2hex(random_bytes(32));
        
        // Save to database
        $stmt = $this->db->prepare(
            "INSERT INTO users (email, password, verification_token, created_at) 
             VALUES (?, ?, ?, NOW())"
        );
        
        $stmt->execute([$email, $hashedPassword, $verificationToken]);
        
        // Send verification email (implementation not shown)
        $this->sendVerificationEmail($email, $verificationToken);
        
        return true;
    }
    
    private function isStrongPassword($password) {
        // Minimum 8 karakter, kombinasi huruf, angka, dan simbol
        return strlen($password) >= 8 
            && preg_match('/[A-Z]/', $password)
            && preg_match('/[a-z]/', $password)
            && preg_match('/[0-9]/', $password)
            && preg_match('/[^A-Za-z0-9]/', $password);
    }
    
    private function emailExists($email) {
        $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);
        return $stmt->rowCount() > 0;
    }
}
?>

Perhatikan penggunaan prepared statements untuk mencegah SQL injection.

Verification token menggunakan cryptographically secure random bytes, bukan sekadar random().

Implementasi Login System dengan Rate Limiting

Login mechanism harus protected dari brute force attacks.

Implementasi rate limiting adalah must-have, bukan optional.

<?php
class LoginSystem {
    private $db;
    private $maxAttempts = 5;
    private $lockoutTime = 900; // 15 minutes in seconds
    
    public function login($email, $password) {
        // Check if account is locked
        if ($this->isAccountLocked($email)) {
            throw new Exception("Akun terkunci. Coba lagi dalam 15 menit.");
        }
        
        // Get user from database
        $stmt = $this->db->prepare(
            "SELECT id, email, password, is_verified 
             FROM users WHERE email = ?"
        );
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        // User not found
        if (!$user) {
            $this->recordFailedAttempt($email);
            throw new Exception("Email atau password salah");
        }
        
        // Check if email is verified
        if (!$user['is_verified']) {
            throw new Exception("Email belum diverifikasi");
        }
        
        // Verify password
        if (!password_verify($password, $user['password'])) {
            $this->recordFailedAttempt($email);
            throw new Exception("Email atau password salah");
        }
        
        // Reset failed attempts on successful login
        $this->resetFailedAttempts($email);
        
        // Create session
        $this->createSession($user['id']);
        
        // Log successful login
        $this->logLoginActivity($user['id'], $_SERVER['REMOTE_ADDR']);
        
        return $user;
    }
    
    private function isAccountLocked($email) {
        $stmt = $this->db->prepare(
            "SELECT failed_attempts, last_failed_at 
             FROM login_attempts WHERE email = ?"
        );
        $stmt->execute([$email]);
        $attempt = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$attempt) {
            return false;
        }
        
        $timeSinceLastFail = time() - strtotime($attempt['last_failed_at']);
        
        if ($attempt['failed_attempts'] >= $this->maxAttempts 
            && $timeSinceLastFail < $this->lockoutTime) {
            return true;
        }
        
        return false;
    }
    
    private function recordFailedAttempt($email) {
        $stmt = $this->db->prepare(
            "INSERT INTO login_attempts (email, failed_attempts, last_failed_at)
             VALUES (?, 1, NOW())
             ON DUPLICATE KEY UPDATE 
             failed_attempts = failed_attempts + 1,
             last_failed_at = NOW()"
        );
        $stmt->execute([$email]);
    }
    
    private function resetFailedAttempts($email) {
        $stmt = $this->db->prepare(
            "DELETE FROM login_attempts WHERE email = ?"
        );
        $stmt->execute([$email]);
    }
}
?>

Rate limiting ini mencegah attacker mencoba ribuan kombinasi password dalam waktu singkat.

Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika Anda. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.

Session Management yang Secure

Session management yang buruk adalah pintu masuk untuk session hijacking dan fixation attacks.

Implementasi yang benar harus regenerate session ID dan set proper cookie flags.

<?php
class SecureSession {
    
    public function __construct() {
        // Set secure session configuration
        ini_set('session.cookie_httponly', 1);
        ini_set('session.use_only_cookies', 1);
        ini_set('session.cookie_secure', 1); // Requires HTTPS
        ini_set('session.cookie_samesite', 'Strict');
        
        session_start();
    }
    
    public function create($userId) {
        // Regenerate session ID to prevent fixation
        session_regenerate_id(true);
        
        // Store user data
        $_SESSION['user_id'] = $userId;
        $_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        $_SESSION['created_at'] = time();
        $_SESSION['last_activity'] = time();
    }
    
    public function validate() {
        // Check if session exists
        if (!isset($_SESSION['user_id'])) {
            return false;
        }
        
        // Validate IP address (optional, can cause issues with mobile)
        if ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR']) {
            $this->destroy();
            return false;
        }
        
        // Validate user agent
        if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
            $this->destroy();
            return false;
        }
        
        // Check session timeout (30 minutes of inactivity)
        $timeout = 1800;
        if (time() - $_SESSION['last_activity'] > $timeout) {
            $this->destroy();
            return false;
        }
        
        // Update last activity
        $_SESSION['last_activity'] = time();
        
        // Regenerate session ID periodically (every 30 minutes)
        if (time() - $_SESSION['created_at'] > 1800) {
            session_regenerate_id(true);
            $_SESSION['created_at'] = time();
        }
        
        return true;
    }
    
    public function destroy() {
        $_SESSION = array();
        
        if (isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/');
        }
        
        session_destroy();
    }
    
    public function getUserId() {
        return $_SESSION['user_id'] ?? null;
    }
}
?>

Session timeout dan periodic regeneration mencegah long-lived session vulnerabilities.

Validasi IP dan user agent menambah layer proteksi meski bisa menimbulkan issue di beberapa kasus edge.

CSRF Protection Implementation

Cross-Site Request Forgery adalah salah satu attack vector yang sering dilupakan developer.

Implementasi CSRF token adalah wajib untuk setiap form yang mengubah data.

<?php
class CSRFProtection {
    
    public static function generateToken() {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public static function validateToken($token) {
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }
        
        // Use hash_equals to prevent timing attacks
        return hash_equals($_SESSION['csrf_token'], $token);
    }
    
    public static function getTokenField() {
        $token = self::generateToken();
        return "<input type='hidden' name='csrf_token' value='{$token}'>";
    }
}

// Usage dalam form
?>
<form method="POST" action="/update-profile">
    <?php echo CSRFProtection::getTokenField(); ?>
    <input type="text" name="username">
    <button type="submit">Update</button>
</form>

<?php
// Validation di backend
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!CSRFProtection::validateToken($_POST['csrf_token'] ?? '')) {
        die('CSRF token validation failed');
    }
    // Process form...
}
?>

Penggunaan hash_equals() penting untuk mencegah timing attacks saat compare tokens.

Password Reset Flow yang Aman

Password reset adalah fitur yang rawan disalahgunakan jika tidak di-implement dengan benar.

Token harus cryptographically secure, expire dalam waktu tertentu, dan single-use.

<?php
class PasswordReset {
    private $db;
    private $tokenExpiry = 3600; // 1 hour
    
    public function requestReset($email) {
        // Verify email exists
        $stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);
        $user = $stmt->fetch();
        
        if (!$user) {
            // Don't reveal if email exists or not (security best practice)
            return "Jika email terdaftar, link reset akan dikirim";
        }
        
        // Generate secure token
        $token = bin2hex(random_bytes(32));
        $hashedToken = hash('sha256', $token);
        $expiresAt = date('Y-m-d H:i:s', time() + $this->tokenExpiry);
        
        // Save token to database
        $stmt = $this->db->prepare(
            "INSERT INTO password_resets (user_id, token, expires_at, created_at)
             VALUES (?, ?, ?, NOW())"
        );
        $stmt->execute([$user['id'], $hashedToken, $expiresAt]);
        
        // Send email with reset link
        $resetLink = "https://yoursite.com/reset-password?token=" . $token;
        $this->sendResetEmail($email, $resetLink);
        
        return "Jika email terdafar, link reset akan dikirim";
    }
    
    public function resetPassword($token, $newPassword) {
        // Hash token for lookup
        $hashedToken = hash('sha256', $token);
        
        // Find valid token
        $stmt = $this->db->prepare(
            "SELECT user_id FROM password_resets 
             WHERE token = ? AND expires_at > NOW() AND used_at IS NULL"
        );
        $stmt->execute([$hashedToken]);
        $reset = $stmt->fetch();
        
        if (!$reset) {
            throw new Exception("Token tidak valid atau sudah kadaluarsa");
        }
        
        // Validate new password
        if (!$this->isStrongPassword($newPassword)) {
            throw new Exception("Password terlalu lemah");
        }
        
        // Hash new password
        $hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]);
        
        // Update user password
        $stmt = $this->db->prepare(
            "UPDATE users SET password = ? WHERE id = ?"
        );
        $stmt->execute([$hashedPassword, $reset['user_id']]);
        
        // Mark token as used
        $stmt = $this->db->prepare(
            "UPDATE password_resets SET used_at = NOW() WHERE token = ?"
        );
        $stmt->execute([$hashedToken]);
        
        // Invalidate all user sessions (force re-login)
        $this->invalidateUserSessions($reset['user_id']);
        
        return true;
    }
    
    private function isStrongPassword($password) {
        return strlen($password) >= 8 
            && preg_match('/[A-Z]/', $password)
            && preg_match('/[a-z]/', $password)
            && preg_match('/[0-9]/', $password);
    }
}
?>

Token di-hash sebelum disimpan ke database supaya jika database leaked, attacker tidak bisa langsung pakai token.

Semua session di-invalidate setelah password reset untuk memastikan attacker yang mungkin sudah masuk ter-kick keluar.

Database Schema untuk Authentication System

Schema database yang proper adalah fondasi dari authentication system yang scalable.

CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    is_verified BOOLEAN DEFAULT FALSE,
    verification_token VARCHAR(64) NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_verification (verification_token)
) ENGINE=InnoDB;

CREATE TABLE login_attempts (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    failed_attempts INT DEFAULT 0,
    last_failed_at TIMESTAMP NULL,
    UNIQUE KEY unique_email (email)
) ENGINE=InnoDB;

CREATE TABLE password_resets (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    token VARCHAR(64) NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    used_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_token (token),
    INDEX idx_user (user_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;

CREATE TABLE login_logs (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    ip_address VARCHAR(45) NOT NULL,
    user_agent TEXT,
    logged_in_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id),
    INDEX idx_ip (ip_address),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;

Index pada kolom yang sering di-query sangat penting untuk performa saat aplikasi scale.

Security Logging dan Monitoring

Logging setiap aktivitas authentication membantu detect suspicious behavior dan investigate security incidents.

<?php
class SecurityLogger {
    private $db;
    
    public function logLogin($userId, $ipAddress, $userAgent) {
        $stmt = $this->db->prepare(
            "INSERT INTO login_logs (user_id, ip_address, user_agent)
             VALUES (?, ?, ?)"
        );
        $stmt->execute([$userId, $ipAddress, $userAgent]);
    }
    
    public function logFailedLogin($email, $ipAddress) {
        // Log to file for monitoring
        $logMessage = sprintf(
            "[%s] Failed login attempt - Email: %s, IP: %s\n",
            date('Y-m-d H:i:s'),
            $email,
            $ipAddress
        );
        error_log($logMessage, 3, '/var/log/security.log');
    }
    
    public function detectAnomalies($userId) {
        // Check for login from multiple IPs in short time
        $stmt = $this->db->prepare(
            "SELECT COUNT(DISTINCT ip_address) as ip_count
             FROM login_logs
             WHERE user_id = ? 
             AND logged_in_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)"
        );
        $stmt->execute([$userId]);
        $result = $stmt->fetch();
        
        if ($result['ip_count'] > 3) {
            // Alert: Possible account compromise
            $this->sendSecurityAlert($userId, 'Multiple IPs detected');
        }
    }
}
?>

Anomaly detection sederhana ini bisa menangkap credential stuffing attacks atau account takeovers.

Testing Authentication System

Authentication code harus di-test secara menyeluruh karena ini adalah critical security component.

Test case minimal yang harus ada:

  • Password hashing produces different hashes for same password (salt check)
  • Verify password returns true for correct password
  • Verify password returns false for incorrect password
  • Registration rejects weak passwords
  • Registration rejects duplicate emails
  • Login locks account after max failed attempts
  • Session validation rejects expired sessions
  • Session validation rejects tampered sessions
  • CSRF token validation rejects invalid tokens
  • Password reset token expires after set time
  • Password reset token cannot be reused

Best Practices dan Pitfalls yang Harus Dihindari

Berikut adalah kesalahan umum yang sering dilakukan developer saat implement authentication:

Jangan pernah return spesifik error message seperti "Email tidak ditemukan" vs "Password salah".

Ini memberi attacker informasi tentang valid accounts. Selalu return generic message.

Jangan pernah log password atau sensitive data, bahkan untuk debugging.

Jangan pernah kirim password melalui email, bahkan temporary password.

Selalu gunakan HTTPS untuk semua authentication endpoints.

Selalu implement proper password policy dan enforce di backend, bukan hanya frontend.

Selalu hash token sebelum simpan ke database.

Mengintegrasikan dengan Frontend

Authentication system tidak lengkap tanpa integrasi yang proper dengan frontend.

Untuk aplikasi modern yang pakai JavaScript framework, return JSON response dengan status code yang konsisten.

<?php
class AuthAPI {
    
    public function handleLogin() {
        header('Content-Type: application/json');
        
        try {
            $input = json_decode(file_get_contents('php://input'), true);
            
            $loginSystem = new LoginSystem($this->db);
            $user = $loginSystem->login(
                $input['email'] ?? '',
                $input['password'] ?? ''
            );
            
            http_response_code(200);
            echo json_encode([
                'success' => true,
                'user' => [
                    'id' => $user['id'],
                    'email' => $user['email']
                ]
            ]);
            
        } catch (Exception $e) {
            http_response_code(401);
            echo json_encode([
                'success' => false,
                'message' => $e->getMessage()
            ]);
        }
    }
}
?>

Konsistensi response format memudahkan frontend handle success dan error cases.

Performa dan Scalability Considerations

Saat aplikasi grow, authentication system harus tetap fast dan reliable.

Beberapa optimization yang bisa dilakukan:

Cache user sessions di Redis atau Memcached supaya tidak query database setiap request.

Implement connection pooling untuk database connections.

Gunakan read replicas untuk operasi read-heavy seperti session validation.

Partition login_logs table by date untuk maintain query performance saat data membesar.

Kesimpulan

Membangun authentication system dari nol memang membutuhkan effort lebih dibanding pakai library.

Tapi knowledge dan kontrol yang Anda dapat sangat worth it, terutama untuk aplikasi enterprise atau yang punya requirement khusus.

Key takeaways yang perlu diingat:

  • Selalu hash password dengan algorithm modern seperti bcrypt
  • Implement rate limiting untuk mencegah brute force
  • Gunakan secure session management dengan proper cookie flags
  • Protect semua forms dengan CSRF tokens
  • Implement comprehensive logging untuk security monitoring
  • Test authentication code secara menyeluruh

Authentication adalah gerbang utama aplikasi Anda. Invest waktu untuk membuatnya dengan benar sejak awal.

Dengan mengikuti panduan ini, Anda sudah punya foundation yang solid untuk membangun authentication system yang aman dan scalable tanpa bergantung pada library pihak ketiga.

Selamat coding dan stay secure!

Ajie Kusumadhany
Written by

Ajie Kusumadhany

Founder & Lead Developer KerjaKode. Berpengalaman dalam pengembangan web modern dengan Laravel, React.js, Vue.js, dan teknologi terkini. Passionate tentang coding, teknologi, dan berbagi pengetahuan melalui artikel.

Promo Spesial Hari Ini!

10% DISKON

Promo berakhir dalam:

00 Jam
:
00 Menit
:
00 Detik
Klaim Promo Sekarang!

*Promo berlaku untuk order hari ini

0
User Online
Halo! 👋
Kerjakode Support Online
×

👋 Hai! Pilih layanan yang kamu butuhkan:

Chat WhatsApp Sekarang