Authentication vs authorization — who are you vs what are you allowed to do
Concept
Authentication vs Authorization — two distinct security concepts that are often confused.
Authentication (AuthN): Verifying IDENTITY. "Who are you?" The process of confirming that someone is who they claim to be. Examples: password login, OAuth, JWT verification, API key validation.
Authorization (AuthZ): Verifying PERMISSION. "What are you allowed to do?" After we know who you are, we decide what you can access. Examples: "only admins can delete users", "users can only edit their own posts", "this API key can only read, not write."
The sequence: Authentication MUST happen before authorization. You can't know if someone has permission until you know who they are.
Common mistakes:
- Returning 401 when you mean 403 (and vice versa).
- 401 Unauthorized: You are NOT authenticated. (Ironically named — it really means "unauthenticated.")
- 403 Forbidden: You ARE authenticated but NOT authorized for this action.
- Checking authentication but skipping authorization ("I'm logged in, so I can see any user's data").
In Laravel:
- Authentication:
auth(),Auth::check(),actingAs(), Sanctum/Jetstream/Breeze. - Authorization: Gates (
Gate::allows()), Policies ($this->authorize()),can()middleware.
IDOR (Insecure Direct Object Reference): A common authorization failure. Authenticated user hits /api/orders/999 — if you only check authentication (is logged in?) but not authorization (does this user OWN order 999?), any user can read any order.
Code Example
<?php
// AUTHENTICATION — who are you?
// Check if user is authenticated
if (auth()->check()) {
$user = auth()->user(); // authenticated User model
}
// Route middleware — require authentication
Route::middleware('auth:sanctum')->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
});
// AUTHORIZATION — what can you do?
// Gate — define permission globally
Gate::define('edit-post', function (User $user, Post $post) {
return $user->id === $post->user_id; // user can only edit own posts
});
// Check authorization
if (Gate::allows('edit-post', $post)) {
// can edit
}
if (Gate::denies('edit-post', $post)) {
abort(403); // 403 Forbidden — authenticated but not allowed
}
// Policy — per-model authorization class
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
}
// In controller
class PostController extends Controller
{
public function update(Request $request, Post $post): Response
{
$this->authorize('update', $post); // 403 if not authorized
$post->update($request->validated());
return response()->noContent();
}
public function show(Post $post): Response
{
// IDOR FIX: always check ownership, not just authentication
if ($post->user_id !== auth()->id()) {
abort(403); // 403 not 404 (don't confirm existence of other users' posts)
}
return response()->json($post);
}
}