Companion Guide: This guide belongs to the Laravel kits. Every Laravel kit ships these checks built in. Explore Kit #1 (Filament), Kit #2 (Livewire), and Kit #3 (MongoDB).
Track 02 Lesson 04

Authentication & Password Handling

Curated by Umar Farooq • 13 min read
Quick Answer & Core Takeaway

Authentication is the gatekeeper of your application. Weak authentication allows attackers to impersonate legitimate users, leading to data breaches, financial loss, and reputation damage.

Why Authentication Security Matters

Authentication is the gatekeeper of your application. Weak authentication allows attackers to impersonate legitimate users, leading to data breaches, financial loss, and reputation damage.

Common Authentication Attacks

Password Hashing Fundamentals

What is Password Hashing?

Password hashing converts plain text passwords into irreversible strings. Unlike encryption, hashing cannot be reversed, making stolen hashes useless to attackers.

Cryptographic Hash Functions

MD5 [INSECURE] (Broken)

PHP / Laravel Code
// NEVER USE - Cryptographically broken
$hash = md5('password'); // Always same result, fast to crack

SHA-256 [INSECURE] (Inadequate for passwords)

PHP / Laravel Code
// PROBLEMATIC - Designed for data integrity, not passwords
$hash = hash('sha256', 'password'); // No salt, fast to crack with rainbow tables

bcrypt [SECURE] (Recommended)

[SECURE] Hardened Pattern
// SECURE - Designed specifically for passwords
$hash = password_hash('password', PASSWORD_BCRYPT, [
    'cost' => 12 // Work factor (higher = slower = more secure)
]);

Argon2id [SECURE] (Modern Standard — OWASP 2024 Recommended)

[SECURE] Hardened Pattern
// MOST SECURE — Winner of Password Hashing Competition
// OWASP Authentication Cheat Sheet 2024 parameters:
$hash = password_hash('password', PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64MB minimum
    'time_cost'   => 3,      // 3 iterations (OWASP 2024)
    'threads'     => 1,      // 1 thread (optimal for PHP web)
]);

Password Hashing Best Practices

1. Use Built-in Functions

PHP / Laravel Code
<?php
// Always use password_hash() - it handles salt automatically
$password = 'user-input-password';

$options = [
    'cost' => 12,  // Increase for better security (slower)
];

// Default is bcrypt, but you can specify Argon2
$hash = password_hash($password, PASSWORD_DEFAULT, $options);

// Verify password
if (password_verify($password, $hash)) {
    // Password is correct
}

2. Never Hash Twice

[INSECURE] Vulnerable Pattern
<?php
// WRONG - Double hashing doesn't add security
$hash = md5(sha1('password')); // Still vulnerable

// RIGHT - Single proper hash
$hash = password_hash('password', PASSWORD_ARGON2ID);

3. Use Appropriate Cost Factors

PHP / Laravel Code
<?php
// Balance security vs performance — OWASP 2024 Argon2id targets
$options = [
    // bcrypt cost: 10-14 (doubles time per increase) — minimum 12
    'cost' => 12,

    // Argon2id — OWASP 2024 minimum recommended parameters
    'memory_cost' => 65536,  // 64MB
    'time_cost'   => 3,      // 3 iterations
    'threads'     => 1,      // 1 thread
];

// Target: hash should take 100–500ms on your hardware
$start = microtime(true);
$hash  = password_hash($password, PASSWORD_ARGON2ID, $options);
$time  = microtime(true) - $start;

if ($time > 0.5) {
    // Too slow — reduce memory_cost or time_cost
} elseif ($time < 0.1) {
    // Too fast — increase memory_cost or time_cost
}

Session Management

Session Security Basics

Sessions track authenticated users across requests. Poor session management leads to hijacking and fixation attacks.

Secure Session Configuration

[SECURE] Hardened Pattern
<?php
// Configure sessions securely — ALL settings must be set before session_start()
ini_set('session.cookie_secure', '1');        // HTTPS only
ini_set('session.cookie_httponly', '1');      // Block JavaScript (XSS theft)
ini_set('session.cookie_samesite', 'Strict'); // CSRF protection
ini_set('session.use_only_cookies', '1');     // No session IDs in URLs
ini_set('session.use_strict_mode', '1');      // Reject unrecognized session IDs
ini_set('session.cookie_lifetime', '0');      // Session cookies (expire on browser close)

// Set session name — use sha256, NOT md5 (deprecated)
session_name('SECURE_APP_SESSION');

// Start session
session_start();

Session Regeneration

[SECURE] Hardened Pattern
<?php
class SecureSession
{
    public static function startSecureSession(): void
    {
        // Secure configuration — must be set before session_start()
        ini_set('session.cookie_secure', '1');
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_samesite', 'Strict');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.use_strict_mode', '1');   // NEW: reject unrecognized session IDs

        // sha256 — NOT md5 (md5 is cryptographically broken)
        session_name('secure_app_' . hash('sha256', __FILE__));
        session_start();

        // Regenerate session ID periodically (every 5 minutes)
        if (!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        } elseif (time() - $_SESSION['created'] > 300) {
            session_regenerate_id(true);
            $_SESSION['created'] = time();
        }
    }

    public static function storeUser(int $userId, string $username, string $role): void
    {
        $_SESSION['user_id'] = $userId;
        $_SESSION['username'] = $username;
        $_SESSION['role'] = $role;
        $_SESSION['login_time'] = time();
        $_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
    }

    public static function isValid(): bool
    {
        // Check if session exists
        if (!isset($_SESSION['user_id'])) {
            return false;
        }

        // Check session timeout (30 minutes)
        if (time() - ($_SESSION['login_time'] ?? 0) > 1800) {
            self::destroy();
            return false;
        }

        // Check IP address consistency (optional)
        if (($_SESSION['ip_address'] ?? '') !== $_SERVER['REMOTE_ADDR']) {
            self::destroy();
            return false;
        }

        // Check user agent consistency (helps prevent some attacks)
        if (($_SESSION['user_agent'] ?? '') !== $_SERVER['HTTP_USER_AGENT']) {
            self::destroy();
            return false;
        }

        return true;
    }

    public static function destroy(): void
    {
        // Clear session data
        $_SESSION = [];

        // Delete session cookie
        if (isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/', '', true, true);
        }

        // Destroy session
        session_destroy();
    }
}

Preventing Session Fixation

Session fixation occurs when an attacker sets a victim's session ID.

[SECURE] Hardened Pattern
<?php
class SecureLogin
{
    public function authenticate(string $username, string $password): bool
    {
        // Validate credentials (implementation not shown)
        if ($this->validateCredentials($username, $password)) {
            // CRITICAL: Regenerate session ID after successful login
            session_regenerate_id(true);

            // Store user data in new session
            $_SESSION['user_id'] = $this->getUserId($username);
            $_SESSION['login_time'] = time();

            return true;
        }

        return false;
    }
}

Secure Login Flow

Complete Authentication Process

PHP / Laravel Code
<?php
class AuthenticationManager
{
    private PDO $pdo;
    private int $maxLoginAttempts = 5;
    private int $lockoutTime = 900; // 15 minutes

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function login(string $username, string $password, string $ipAddress): array
    {
        // Check if account is locked
        if ($this->isAccountLocked($username)) {
            return [
                'success' => false,
                'message' => 'Account is temporarily locked due to too many failed attempts'
            ];
        }

        // Get user from database
        $user = $this->getUserByUsername($username);
        if (!$user) {
            $this->recordFailedAttempt($username, $ipAddress);
            return [
                'success' => false,
                'message' => 'Invalid username or password'
            ];
        }

        // Verify password
        if (!password_verify($password, $user['password_hash'])) {
            $this->recordFailedAttempt($username, $ipAddress);
            return [
                'success' => false,
                'message' => 'Invalid username or password'
            ];
        }

        // Check if account is active
        if (!$user['active']) {
            return [
                'success' => false,
                'message' => 'Account is deactivated'
            ];
        }

        // Successful login
        $this->clearFailedAttempts($username);

        // Regenerate session ID
        session_regenerate_id(true);

        // Store user session
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        $_SESSION['role'] = $user['role'];
        $_SESSION['login_time'] = time();
        $_SESSION['ip_address'] = $ipAddress;

        return [
            'success' => true,
            'message' => 'Login successful',
            'user' => [
                'id' => $user['id'],
                'username' => $user['username'],
                'role' => $user['role']
            ]
        ];
    }

    private function getUserByUsername(string $username): ?array
    {
        $stmt = $this->pdo->prepare("
            SELECT id, username, password_hash, role, active
            FROM users
            WHERE username = ? AND active = 1
        ");
        $stmt->execute([$username]);

        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    private function isAccountLocked(string $username): bool
    {
        $stmt = $this->pdo->prepare("
            SELECT COUNT(*) as attempts, MAX(attempt_time) as last_attempt
            FROM login_attempts
            WHERE username = ? AND attempt_time > DATE_SUB(NOW(), INTERVAL 15 MINUTE)
        ");
        $stmt->execute([$username]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return $result['attempts'] >= $this->maxLoginAttempts;
    }

    private function recordFailedAttempt(string $username, string $ipAddress): void
    {
        $stmt = $this->pdo->prepare("
            INSERT INTO login_attempts (username, ip_address, attempt_time)
            VALUES (?, ?, NOW())
        ");
        $stmt->execute([$username, $ipAddress]);
    }

    private function clearFailedAttempts(string $username): void
    {
        $stmt = $this->pdo->prepare("
            DELETE FROM login_attempts
            WHERE username = ?
        ");
        $stmt->execute([$username]);
    }
}

Password Policies

Strong Password Requirements

[SECURE] Hardened Pattern
<?php
class PasswordPolicy
{
    public static function validatePassword(string $password): array
    {
        $errors = [];
        $score = 0;

        // Length check — NIST SP 800-63B 2024: minimum 15 characters
        if (strlen($password) < 15) {
            $errors[] = 'Password must be at least 15 characters (NIST SP 800-63B 2024)';
        } else {
            $score += 20;
        }

        // Character variety
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password must contain uppercase letter';
        } else {
            $score += 20;
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password must contain lowercase letter';
        } else {
            $score += 20;
        }

        if (!preg_match('/\d/', $password)) {
            $errors[] = 'Password must contain number';
        } else {
            $score += 20;
        }

        if (!preg_match('/[@$!%*?&]/', $password)) {
            $errors[] = 'Password must contain special character';
        } else {
            $score += 20;
        }

        // Check against common passwords
        $commonPasswords = [
            'password', '123456', 'qwerty', 'admin', 'letmein',
            'welcome', 'monkey', '123456789', 'iloveyou', 'princess'
        ];

        if (in_array(strtolower($password), $commonPasswords)) {
            $errors[] = 'Password is too common';
            $score = 0;
        }

        // Check for repeated characters
        if (preg_match('/(.)\1{2,}/', $password)) {
            $errors[] = 'Password cannot contain repeated characters';
        }

        // Check for sequential characters
        if (preg_match('/123|234|345|456|567|678|789|abc|bcd|cde|def/', $password)) {
            $errors[] = 'Password cannot contain sequential characters';
        }

        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'strength' => $score,
            'level' => self::getStrengthLevel($score)
        ];
    }

    private static function getStrengthLevel(int $score): string
    {
        if ($score >= 80) return 'Very Strong';
        if ($score >= 60) return 'Strong';
        if ($score >= 40) return 'Medium';
        if ($score >= 20) return 'Weak';
        return 'Very Weak';
    }

    public static function generateSecurePassword(int $length = 12): string
    {
        $lowercase = 'abcdefghijklmnopqrstuvwxyz';
        $uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $numbers = '0123456789';
        $symbols = '@$!%*?&';

        $allChars = $lowercase . $uppercase . $numbers . $symbols;

        // Ensure at least one of each type
        // [SECURE] random_int() is cryptographically secure — NEVER use rand() for security purposes
        $password  = '';
        $password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
        $password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
        $password .= $numbers[random_int(0, strlen($numbers) - 1)];
        $password .= $symbols[random_int(0, strlen($symbols) - 1)];

        // Fill the rest — cryptographically secure
        for ($i = 4; $i < $length; $i++) {
            $password .= $allChars[random_int(0, strlen($allChars) - 1)];
        }

        // Shuffle to avoid predictable patterns
        return str_shuffle($password);
    }
}

Laravel Authentication

Using Laravel Sanctum for API Authentication

PHP / Laravel Code
<?php
// config/auth.php — Laravel 13 guards
'guards' => [
    'web' => [
        'driver'   => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver'   => 'sanctum',  // Laravel Sanctum 4.x
        'provider' => null,
    ],
],

User Model with Secure Authentication

[SECURE] Hardened Pattern
<?php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'email_verified_at',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password'          => 'hashed',     // Laravel 11+ automatic hashing via HashedCast
            'two_factor_secret' => 'encrypted',  // Encrypted at rest using APP_KEY
        ];
    }

    // Secure password hashing
    public function setPasswordAttribute(string $password): void
    {
        $this->attributes['password'] = password_hash($password, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3,
        ]);
    }

    // Custom password verification
    public function verifyPassword(string $password): bool
    {
        return password_verify($password, $this->password);
    }
}

Secure Registration Controller

PHP / Laravel Code
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller
{
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => [
                'required',
                'string',
                'min:8',
                'confirmed',
                'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/'
            ],
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ], 422);
        }

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password), // Or use setPasswordAttribute
        ]);

        // Generate API token if using Sanctum
        $token = $user->createToken('API Token')->plainTextToken;

        return response()->json([
            'success' => true,
            'message' => 'User registered successfully',
            'user' => $user,
            'token' => $token
        ], 201);
    }
}

Secure Login Controller

PHP / Laravel Code
<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
        ]);

        // Rate limiting
        if (RateLimiter::tooManyAttempts($this->throttleKey($request), 5)) {
            $seconds = RateLimiter::availableIn($this->throttleKey($request));

            throw ValidationException::withMessages([
                'email' => "Too many login attempts. Please try again in {$seconds} seconds."
            ]);
        }

        if (!Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey($request), 300); // 5 minutes

            throw ValidationException::withMessages([
                'email' => 'Invalid credentials.'
            ]);
        }

        RateLimiter::clear($this->throttleKey($request));

        $request->session()->regenerate();

        return response()->json([
            'success' => true,
            'message' => 'Login successful',
            'user' => Auth::user()
        ]);
    }

    public function logout(Request $request)
    {
        Auth::logout();

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return response()->json([
            'success' => true,
            'message' => 'Logged out successfully'
        ]);
    }

    private function throttleKey(Request $request): string
    {
        return 'login:' . $request->ip() . ':' . $request->input('email');
    }
}

Multi-Factor Authentication (MFA)

TOTP Implementation

PHP / Laravel Code
<?php
// composer require pragmarx/google2fa

use PragmaRX\Google2FA\Google2FA;

class TwoFactorAuth
{
    private Google2FA $google2fa;

    public function __construct()
    {
        $this->google2fa = new Google2FA();
    }

    public function generateSecret(): string
    {
        return $this->google2fa->generateSecretKey();
    }

    public function getQRCodeUrl(string $secret, string $email, string $issuer = 'MyApp'): string
    {
        return $this->google2fa->getQRCodeUrl($issuer, $email, $secret);
    }

    public function verifyCode(string $secret, string $code): bool
    {
        return $this->google2fa->verifyKey($secret, $code);
    }
}

// Usage in User model
class User extends Authenticatable
{
    protected $fillable = ['name', 'email', 'password', 'two_factor_secret', 'two_factor_enabled'];

    public function enableTwoFactor(): void
    {
        $this->two_factor_secret = app(TwoFactorAuth::class)->generateSecret();
        $this->two_factor_enabled = false; // Set to true after verification
        $this->save();
    }

    public function verifyTwoFactorCode(string $code): bool
    {
        return app(TwoFactorAuth::class)->verifyCode($this->two_factor_secret, $code);
    }
}

Security Monitoring and Logging

Authentication Event Logging

PHP / Laravel Code
<?php
class AuthLogger
{
    public static function logSuccessfulLogin(int $userId, string $ip, string $userAgent): void
    {
        error_log(sprintf(
            "[AUTH SUCCESS] User ID: %d, IP: %s, User-Agent: %s, Time: %s",
            $userId,
            $ip,
            $userAgent,
            date('Y-m-d H:i:s')
        ));
    }

    public static function logFailedLogin(string $username, string $ip, string $reason): void
    {
        error_log(sprintf(
            "[AUTH FAILED] Username: %s, IP: %s, Reason: %s, Time: %s",
            $username,
            $ip,
            $reason,
            date('Y-m-d H:i:s')
        ));
    }

    public static function logSuspiciousActivity(string $type, array $data): void
    {
        error_log(sprintf(
            "[SECURITY %s] %s",
            $type,
            json_encode($data)
        ));
    }
}

Summary: Authentication Security Checklist

Password Security

Session Management

Login Security

Advanced Security

Next Steps

Now that you understand authentication and password security, explore:

Remember: Authentication is your application's first line of defense. Implement it properly to protect your users and data!

AI Assistant Shortcuts
Ask AI about this topic
← Prev: Laravel 13 Architecture Security
Next: Session Security & Fixation Defense →
UF

Curated by Umar Farooq

Senior Full-Stack & Security Engineer with 5+ years of production experience in Laravel, PHP, and modern web application security. Author of the Laravel AI Security Masterclass.