0

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):

  1. User logs in. Server creates a session record in storage (database/Redis). Returns a session ID in a cookie.
  2. Each request: browser sends cookie → server looks up session ID → finds user.
  3. Logout: delete the session record. Immediate invalidation.
  4. Pros: Simple, immediate revocation, built into Laravel naturally.
  5. Cons: Requires shared session storage for horizontal scaling (use Redis). Session ID in cookie needs CSRF protection.
  6. Best for: Traditional web apps, same-origin requests, apps where you own the frontend.

JWT (JSON Web Token) (stateless):

  1. User logs in. Server signs a payload {user_id, role, exp} with a secret key. Returns the signed JWT.
  2. Each request: client sends JWT in Authorization: Bearer <token>. Server verifies signature + expiry. No DB lookup.
  3. Logout: JWTs can't be invalidated without a blocklist (defeating statelessness). Common approach: short expiry (15min) + refresh tokens.
  4. Pros: Stateless (no shared storage needed), works across domains, good for mobile/SPA.
  5. Cons: Cannot instantly revoke tokens. Larger than session IDs. Must handle refresh token rotation.
  6. Best for: API-only backends, mobile apps, microservices (service-to-service).

OAuth2 (delegation / authorization):

  1. "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.
  2. Roles: Resource Owner (user), Client (your app), Authorization Server (GitHub/Google), Resource Server (API that has user's data).
  3. Grants: Authorization Code (web apps), Client Credentials (server-to-server), Device Flow (smart TVs).
  4. 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');
});