Problem Statement
You need to implement a secure authentication system that supports multiple clients (web, mobile, API), protects against common attacks, and provides a good user experience.
Authentication Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ Client Applications │
├─────────────────┬─────────────────┬─────────────────┬──────────────────┤
│ Web (SPA) │ Mobile App │ 3rd Party API │ Internal API │
│ HTTP-Only │ Secure Token │ OAuth 2.0 │ API Key + │
│ Cookies │ Storage │ Client Creds │ mTLS │
└────────┬────────┴────────┬────────┴────────┬────────┴────────┬─────────┘
│ │ │ │
└─────────────────┴────────┬────────┴─────────────────┘
│
┌─────────▼─────────┐
│ API Gateway │
│ Rate Limiting │
│ Token Validation│
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ Auth Service │
│ JWT Issuer │
│ Session Mgmt │
└─────────┬─────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ User DB │ │ Redis │ │ OAuth │
│ (hashed) │ │ (sessions) │ │ Providers │
└─────────────┘ └─────────────┘ └─────────────┘
Strategy 1: JWT + HTTP-Only Cookies (Web Applications)
Backend Implementation (Laravel)
// config/sanctum.php
'expiration' => 60, // Token expires in 60 minutes
// app/Http/Controllers/AuthController.php
class AuthController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($credentials)) {
// Use a generic message to prevent user enumeration.
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$user = Auth::user();
// Rate-limit failed attempts.
RateLimiter::hit('login:' . $request->ip(), 300);
// Create token.
$token = $user->createToken('auth_token')->plainTextToken;
// Set an HTTP-only cookie for web clients.
$cookie = cookie(
'auth_token',
$token,
60, // minutes
'/',
null,
true, // secure (HTTPS only)
true, // httpOnly
false,
'Strict' // sameSite
);
return response()->json([
'user' => $user,
'message' => 'Login successful'
])->withCookie($cookie);
}
public function logout(Request $request)
{
// Revoke current token.
$request->user()->currentAccessToken()->delete();
// Clear cookie.
$cookie = cookie()->forget('auth_token');
return response()->json(['message' => 'Logged out'])
->withCookie($cookie);
}
public function refresh(Request $request)
{
$user = $request->user();
// Revoke old token.
$user->currentAccessToken()->delete();
// Issue new token.
$token = $user->createToken('auth_token')->plainTextToken;
$cookie = cookie('auth_token', $token, 60, '/', null, true, true, false, 'Strict');
return response()->json(['message' => 'Token refreshed'])
->withCookie($cookie);
}
}
Token Validation Middleware
// app/Http/Middleware/ValidateToken.php
class ValidateToken
{
public function handle($request, Closure $next)
{
// Check the cookie first, then the Authorization header.
$token = $request->cookie('auth_token')
?? $request->bearerToken();
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$accessToken = PersonalAccessToken::findToken($token);
if (!$accessToken || $accessToken->expires_at < now()) {
return response()->json(['error' => 'Token expired'], 401);
}
// Set authenticated user.
Auth::setUser($accessToken->tokenable);
return $next($request);
}
}
Frontend Implementation (Vue.js/React)
// api/auth.js
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true, // Important: Send cookies with requests.
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
// Add CSRF token for Laravel.
api.interceptors.request.use(config => {
const token = document.querySelector('meta[name="csrf-token"]')?.content;
if (token) {
config.headers['X-CSRF-TOKEN'] = token;
}
return config;
});
// Handle token expiration.
api.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// Try to refresh token.
try {
await api.post('/auth/refresh');
return api.request(error.config);
} catch (refreshError) {
// Redirect to login.
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export const auth = {
login: (email, password) => api.post('/auth/login', { email, password }),
logout: () => api.post('/auth/logout'),
getUser: () => api.get('/auth/user'),
};
Strategy 2: OAuth 2.0 / Social Login
Laravel Socialite Configuration
// config/services.php
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URL'),
],
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),
'redirect' => env('APPLE_REDIRECT_URL'),
],
// app/Http/Controllers/SocialAuthController.php
class SocialAuthController extends Controller
{
public function redirect($provider)
{
// Validate provider.
if (!in_array($provider, ['google', 'apple', 'facebook'])) {
abort(404);
}
return Socialite::driver($provider)
->stateless()
->redirect();
}
public function callback($provider)
{
try {
$socialUser = Socialite::driver($provider)->stateless()->user();
} catch (Exception $e) {
return redirect('/login')->with('error', 'Authentication failed');
}
// Find or create user.
$user = User::updateOrCreate(
['email' => $socialUser->getEmail()],
[
'name' => $socialUser->getName(),
'provider' => $provider,
'provider_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar(),
]
);
// Create token.
$token = $user->createToken('social_auth')->plainTextToken;
// Redirect to frontend with token.
return redirect(env('FRONTEND_URL') . '/auth/callback?token=' . $token);
}
}
Strategy 3: API Key Authentication (Server-to-Server)
API Key Implementation
// app/Models/ApiKey.php
class ApiKey extends Model
{
protected $fillable = ['name', 'key', 'permissions', 'rate_limit', 'expires_at'];
protected $casts = ['permissions' => 'array'];
public static function generate($name, $permissions = [])
{
$key = Str::random(32);
return self::create([
'name' => $name,
'key' => hash('sha256', $key),
'permissions' => $permissions,
'rate_limit' => 1000,
]);
}
public static function findByKey($key)
{
return self::where('key', hash('sha256', $key))->first();
}
}
// app/Http/Middleware/ValidateApiKey.php
class ValidateApiKey
{
public function handle($request, Closure $next, ...$permissions)
{
$key = $request->header('X-API-Key');
if (!$key) {
return response()->json(['error' => 'API key required'], 401);
}
$apiKey = ApiKey::findByKey($key);
if (!$apiKey) {
return response()->json(['error' => 'Invalid API key'], 401);
}
if ($apiKey->expires_at && $apiKey->expires_at < now()) {
return response()->json(['error' => 'API key expired'], 401);
}
// Check permissions.
foreach ($permissions as $permission) {
if (!in_array($permission, $apiKey->permissions ?? [])) {
return response()->json(['error' => 'Insufficient permissions'], 403);
}
}
// Rate limiting.
$rateLimitKey = 'api_key:' . $apiKey->id;
if (RateLimiter::tooManyAttempts($rateLimitKey, $apiKey->rate_limit)) {
return response()->json(['error' => 'Rate limit exceeded'], 429);
}
RateLimiter::hit($rateLimitKey, 3600);
$request->merge(['api_key' => $apiKey]);
return $next($request);
}
}
Password Security
Secure Password Hashing
// Always use bcrypt or argon2.
$hashedPassword = Hash::make($password);
// Verify.
if (Hash::check($plainPassword, $hashedPassword)) {
// Password matches.
}
// Check if rehash is needed (algorithm upgrade).
if (Hash::needsRehash($hashedPassword)) {
$user->password = Hash::make($plainPassword);
$user->save();
}
Password Validation Rules
// app/Rules/SecurePassword.php
class SecurePassword implements Rule
{
public function passes($attribute, $value)
{
return strlen($value) >= 12
&& preg_match('/[A-Z]/', $value)
&& preg_match('/[a-z]/', $value)
&& preg_match('/[0-9]/', $value)
&& preg_match('/[^A-Za-z0-9]/', $value)
&& !$this->isCommonPassword($value);
}
private function isCommonPassword($password)
{
$common = ['password123', '123456789', 'qwerty123'];
return in_array(strtolower($password), $common);
}
}
Multi-Factor Authentication (MFA)
TOTP Implementation
// Using pragmarx/google2fa.
use PragmaRX\Google2FA\Google2FA;
class MfaController extends Controller
{
public function enable(Request $request)
{
$google2fa = new Google2FA();
// Generate secret.
$secret = $google2fa->generateSecretKey();
// Store temporarily in session.
session(['mfa_secret' => $secret]);
// Generate QR code URL.
$qrCodeUrl = $google2fa->getQRCodeUrl(
config('app.name'),
$request->user()->email,
$secret
);
return response()->json([
'secret' => $secret,
'qr_code_url' => $qrCodeUrl,
]);
}
public function verify(Request $request)
{
$google2fa = new Google2FA();
$secret = session('mfa_secret');
if (!$google2fa->verifyKey($secret, $request->code)) {
return response()->json(['error' => 'Invalid code'], 422);
}
// Save MFA to user.
$request->user()->update([
'mfa_secret' => encrypt($secret),
'mfa_enabled' => true,
]);
// Generate backup codes.
$backupCodes = collect(range(1, 10))->map(fn() => Str::random(8));
$request->user()->backupCodes()->createMany(
$backupCodes->map(fn($code) => ['code' => Hash::make($code)])
);
session()->forget('mfa_secret');
return response()->json([
'message' => 'MFA enabled',
'backup_codes' => $backupCodes,
]);
}
}
Session Security
Redis Session Configuration
// config/session.php
'driver' => 'redis',
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => true,
'secure' => true,
'http_only' => true,
'same_site' => 'lax',
Session Invalidation on Security Events
// Invalidate all sessions on password change.
class PasswordController extends Controller
{
public function update(Request $request)
{
$user = $request->user();
$user->password = Hash::make($request->new_password);
$user->save();
// Revoke all tokens.
$user->tokens()->delete();
// Invalidate all sessions.
DB::table('sessions')
->where('user_id', $user->id)
->delete();
// Create new session for current request.
Auth::login($user);
}
}
Rate Limiting
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
'throttle:api',
],
];
// app/Providers/RouteServiceProvider.php
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perMinute(10)->by($request->input('email')),
];
});
Security Headers
// app/Http/Middleware/SecurityHeaders.php
class SecurityHeaders
{
public function handle($request, Closure $next)
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
$response->headers->set('Strict-Transport-Security',
'max-age=31536000; includeSubDomains');
return $response;
}
}
Security Checklist
Authentication
- [ ] Use HTTPS everywhere
- [ ] Store passwords with bcrypt/argon2
- [ ] Implement account lockout after failed attempts
- [ ] Use HTTP-only, Secure, SameSite cookies
- [ ] Implement CSRF protection
- [ ] Add MFA support
- [ ] Generate secure session IDs
- [ ] Invalidate sessions on logout and password change
API Security
- [ ] Validate and sanitize all inputs
- [ ] Use parameterized queries (prevent SQL injection)
- [ ] Implement rate limiting
- [ ] Use API keys for server-to-server
- [ ] Log authentication events
- [ ] Set proper CORS headers
Token Security
- [ ] Short token expiration (15-60 minutes)
- [ ] Implement token refresh mechanism
- [ ] Store refresh tokens securely
- [ ] Revoke tokens on logout
- [ ] Don't store tokens in localStorage (use HTTP-only cookies)
The Senior Auth Mindset
Senior engineers treat authentication and access control as critical systems, not as add-ons. They design for secure defaults, operational visibility, and easy recovery.
Key Principles
Threat Modeling First: Define what you are protecting and who might attack it. Identify entry points such as login endpoints, password reset flows, and third-party integrations. This informs how strong your authentication controls need to be.
Layered Security: Security is multi-layered—physical, network, platform, application, and data controls should all work together. When the entire stack is aligned, authentication becomes a predictable boundary rather than a fragile edge.
Monitor Auth Health: Authentication systems should expose metrics: login success rate, failed login rate, MFA failure rate, and token refresh errors. Alert on spikes that could indicate attacks or outages. Visibility is essential because auth problems often look like general system outages to users.
Common Pitfalls to Avoid
- Mixing authentication and authorization logic in a single step
- Long-lived tokens without rotation
- Weak password reset flows
- Missing audit logs for sensitive actions
- Overly broad permissions that become permanent
Balance Security with UX
Authentication that is too strict can drive users away, while authentication that is too lax invites abuse. Provide clear error messages, avoid unnecessary re-authentication, and guide users through secure flows without frustration. Good UX is a security feature—users are more likely to follow secure practices when the system is clear and responsive.
That trust is difficult to rebuild once lost. Protect it.
Related Wiki Articles