Skip to content

← Writing

engineering

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.