Auth and Permissions: Who Are You, and What Can You Do?
· Jerwin Arnado ·
Part four of the full-stack series. Two words people constantly blur, and the distinction is the whole game:
- Authentication (authn): who are you? Proving identity — login.
- Authorization (authz): what can you do? Granting access — permissions.
You need both, in that order, and you need them on the server, never the client. A hidden button is UX; a denied request is security. This is how I build the layer in Laravel.
Authentication: proving identity
The baseline is email + password, and the only rule that matters for passwords: never store them, store a hash. A one-way, slow, salted hash — bcrypt or argon2 — so a database leak doesn’t hand over everyone’s password. Laravel does this by default; the sin is undoing it.
// Hashing on the way in (bcrypt by default, salted automatically)
$user = User::create([
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Verifying on login — constant-time compare, no plain text anywhere
if (! Hash::check($request->password, $user->password)) {
return back()->withErrors(['email' => 'Invalid credentials.']);
}
Layer on what the threat model needs: rate-limited login (covered in the rate-limiting post), email verification, and 2FA for anything sensitive. And in 2026, seriously consider passkeys — phishing-resistant by design, no shared secret to leak.
Sessions vs tokens: pick by client
How you remember a logged-in user depends on who’s calling:
| Mechanism | Best for | How it works |
|---|---|---|
| Session cookie | server-rendered web app, same-domain SPA | server stores session, browser holds the cookie |
| API token | mobile app, third-party API client | client sends a bearer token each request |
In Laravel, Sanctum covers both — cookie sessions for your own first-party frontend, personal-access tokens for APIs — without dragging in a full OAuth server you don’t need. Reach for OAuth (Passport) only when third parties need delegated access to your users’ data. Most apps never do.
Authorization: roles are not enough
The trap: modeling permissions as roles alone. “Admin can do anything, user can do less” works until the real question shows up — can this user edit this specific post? That’s not a role, it’s a rule about a row. Laravel Policies put that rule in one place:
class PostPolicy
{
public function update(User $user, Post $post): bool
{
// ownership, not just role — the question roles can't answer
return $user->id === $post->user_id
|| $user->hasRole('editor');
}
}
Then enforce it the same way everywhere — controller, Blade, API resource:
$this->authorize('update', $post); // throws 403 if denied
Roles answer “what kind of user is this.” Policies answer “can this user do this thing to this object.” Real apps need both, and the row-level question is the one that bites — we go deeper in the security post.
Caveats and best practices
- Authorize on the server, every time. The frontend hiding a control is convenience, not enforcement. The check that counts is the one the client can’t skip.
- Default deny. New ability, new endpoint, new field → locked until you explicitly allow it. The expensive bugs are the ones that default open.
- Don’t leak existence via errors. “No such email” vs “wrong password” tells an attacker which emails are registered. Same message for both.
- Sessions expire; tokens get scoped and revoked. Long-lived all-powerful tokens are a liability. Short TTLs, narrow scopes, a revoke path.
- Log auth events. Logins, failures, permission denials. When something goes wrong this trail is the investigation — see error tracking and logs.
Conclusion
Authn → who: hashed passwords, Sanctum, 2FA/passkeys
Authz → what: roles for kind, policies for the row
Where → server-side, default-deny, every request
Trail → log logins, failures, denials
Authentication and authorization are where a small slip becomes a breach report, so the discipline is worth it: hash everything, check on the server, default to no, and never let roles stand in for row-level rules. Next: hosting and deployment — getting all of this onto a server the world can reach.