Design an authentication system — session vs JWT vs OAuth2
Advanced5 min read·eng-11-004
interviewsecurity
Concept
Authentication system design — session, JWT, and OAuth2 compared at the architectural level.
Session-based authentication (stateful):
- User logs in. Server creates a session record in storage (database/Redis). Returns a session ID in a cookie.
- Each request: browser sends cookie → server looks up session ID → finds user.
- Logout: delete the session record. Immediate invalidation.
- Pros: Simple, immediate revocation, built into Laravel naturally.
- Cons: Requires shared session storage for horizontal scaling (use Redis). Session ID in cookie needs CSRF protection.
- Best for: Traditional web apps, same-origin requests, apps where you own the frontend.
JWT (JSON Web Token) (stateless):
- User logs in. Server signs a payload
{user_id, role, exp}with a secret key. Returns the signed JWT. - Each request: client sends JWT in
Authorization: Bearer <token>. Server verifies signature + expiry. No DB lookup. - Logout: JWTs can't be invalidated without a blocklist (defeating statelessness). Common approach: short expiry (15min) + refresh tokens.
- Pros: Stateless (no shared storage needed), works across domains, good for mobile/SPA.
- Cons: Cannot instantly revoke tokens. Larger than session IDs. Must handle refresh token rotation.
- Best for: API-only backends, mobile apps, microservices (service-to-service).
OAuth2 (delegation / authorization):
- "Log in with GitHub": redirects user to GitHub → user authorizes → GitHub sends a code → your server exchanges code for an access token → you get user's profile from GitHub.
- Roles: Resource Owner (user), Client (your app), Authorization Server (GitHub/Google), Resource Server (API that has user's data).
- Grants: Authorization Code (web apps), Client Credentials (server-to-server), Device Flow (smart TVs).
- Best for: "Login with Google/GitHub", letting third-party apps access your API on behalf of users.
Laravel implementations: Sanctum (session + SPA tokens + mobile API tokens), Passport (full OAuth2 server), Fortify (authentication scaffolding), Socialite (OAuth2 client for Google/GitHub etc.).
Code Example
php
<?php
// ============================================================
// SESSION AUTH — Laravel Sanctum for SPAs
// ============================================================
// web.php routes — uses cookie-based session auth
Route::post('/login', function (\Illuminate\Http\Request $request) {
$request->validate(['email' => 'required|email', 'password' => 'required']);
if (!\Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
throw \Illuminate\Validation\ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$request->session()->regenerate(); // prevent session fixation
return response()->json(['message' => 'Logged in']);
});
// ============================================================
// API TOKEN AUTH — Laravel Sanctum (stateless)
// ============================================================
Route::post('/api/login', function (\Illuminate\Http\Request $request) {
$user = \App\Models\User::where('email', $request->email)->first();
if (!$user || !\Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
return response()->json(['token' => $token->plainTextToken]);
// Client sends: Authorization: Bearer <token>
});
// ============================================================
// JWT — manual implementation (or use tymon/jwt-auth package)
// ============================================================
class JwtService
{
private readonly string $secret;
public function __construct() { $this->secret = config('app.key'); }
public function encode(array $payload): string
{
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode(array_merge($payload, ['exp' => time() + 900])));
$signature = base64url_encode(hash_hmac('sha256', "{$header}.{$payload}", $this->secret, true));
return "{$header}.{$payload}.{$signature}";
}
public function decode(string $token): array
{
[$header, $payload, $sig] = explode('.', $token);
$expectedSig = base64url_encode(hash_hmac('sha256', "{$header}.{$payload}", $this->secret, true));
if (!hash_equals($expectedSig, $sig)) throw new \RuntimeException('Invalid signature');
$claims = json_decode(base64url_decode($payload), true);
if ($claims['exp'] < time()) throw new \RuntimeException('Token expired');
return $claims;
}
}
// ============================================================
// OAUTH2 CLIENT — Laravel Socialite (login with GitHub)
// ============================================================
Route::get('/auth/github', fn() => \Socialite::driver('github')->redirect());
Route::get('/auth/github/callback', function () {
$githubUser = \Socialite::driver('github')->user();
$user = \App\Models\User::updateOrCreate(
['github_id' => $githubUser->getId()],
['name' => $githubUser->getName(), 'email' => $githubUser->getEmail()],
);
\Auth::login($user, remember: true);
return redirect('/dashboard');
});