Authentication vs authorization

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: Two words that sound alike, mean different things, and get mixed up all the time. Authentication = who you are. Authorization = what you’re allowed to do. Lock this distinction in early.


In plain English

Walk into an office building. The security guard checks your photo ID — that’s authentication. You’ve proven you’re George. Then the receptionist looks up your access level — “George, you can go to floors 1, 2, and 5 but not the executive floor” — that’s authorization. Two separate questions, two separate answers, two separate systems.

Web apps work the same way:

  • Authentication (often shortened to authn) — proving who you are. Logging in with an email and password. Tapping a fingerprint. Clicking a magic link in your email.
  • Authorization (often shortened to authz) — checking what you’re allowed to do. “Is George allowed to delete this post?” “Can George see this private dashboard?”

You always authenticate first. Authorization happens on every subsequent action. A logged-in user gets authenticated once; their authorization gets checked dozens of times as they click around the app.

These two get mixed up constantly — even by experienced developers. “Auth bug” could mean either. When you hear someone say “the auth system is broken,” ask: which part? It saves hours.


Why it matters

  • Most security bugs live in this layer. “I can see other users’ data” is an authorization bug. “Anyone can log in as anyone” is an authentication bug. Knowing which lets you find the fix.
  • The technical pieces are different. Authentication involves passwords, hashing, sessions, tokens. Authorization involves rules, roles, permissions, RLS. Don’t conflate them when designing.
  • Most modern auth services handle authentication for you (Supabase Auth, Clerk, Auth0, NextAuth) — but authorization is yours to design. The service can verify identity; only you know who should see what.
  • In Supabase + RLS, both happen in different places. Supabase Auth verifies the user (authn). RLS policies enforce row access (authz). The two combine cleanly: RLS policies use auth.uid() to know who’s asking.

How authentication works (the short version)

The user proves they are who they claim to be. Common methods:

MethodHow it worksStrength
Email + passwordUser remembers a password; server checks the hashOK if password is strong
Magic linkUser clicks a link emailed to themOK if email is secure
Social login (OAuth)“Sign in with Google” — Google vouches for themStrong; offloads complexity
Passkey / WebAuthnCryptographic key on user’s device (Face ID, etc.)Strongest, phishing-resistant
Phone / SMS OTPOne-time code via SMSOK but vulnerable to SIM-swap
MFA (multi-factor)Two of the aboveMuch stronger than any single

The result of successful authentication is usually a session (a cookie identifying the user) or a token (a JWT). Either way, future requests can identify “George is the one asking” without him retyping his password.

See Sessions & cookies and JWT for the mechanics.


How authorization works (the short version)

For each protected action, the system asks: “is this user allowed to do this?” Several patterns:

Role-Based Access Control (RBAC)

Users have roles (admin, editor, viewer). Each role has permissions. “Editors can publish posts” → if user.role === “editor”, allow publish.

Attribute-Based Access Control (ABAC)

Permissions depend on attributes of the user, resource, and action. “A user can edit their own posts only” → if post.user_id === user.id, allow edit.

Row-Level Security (Postgres / Supabase)

The database itself enforces who can see / change which rows. Write the rule once in SQL; every query is filtered automatically. See Row-Level Security.

Policy engines

For complex enterprise: Open Policy Agent, Cedar, etc. Overkill for most webapps; relevant if you have many roles, complex rules, or compliance audits.

For a typical Next.js + Supabase project:

  • Authentication is Supabase Auth
  • Authorization is RLS policies, plus occasional server-side checks in code

A concrete example: a blog with users

User George is logged in. Authentication says: “this is George, user_id abc-123.”

Now George tries different actions. Authorization rules:

ActionAllowed?Why
Read public postsYesAnyone can
Read his own draftsYesauth.uid() = post.author_id
Read someone else’s draftsNoSame rule, different user
Edit his own published postYesSame rule
Edit someone else’s postNoAuthz rule
Delete the entire posts tableNoNo DB permission
Promote himself to adminNoAdmin promotion not authorized

George can authenticate perfectly and STILL be denied 6 of 7 actions. Authentication and authorization are separate gates.

In SQL:

-- Anyone can read published posts
CREATE POLICY "Public reads" ON posts FOR SELECT
  USING (published = true);
 
-- Authenticated users read their own drafts too
CREATE POLICY "Authors read own drafts" ON posts FOR SELECT
  USING (auth.uid() = author_id);
 
-- Authors edit their own posts
CREATE POLICY "Authors edit own" ON posts FOR UPDATE
  USING (auth.uid() = author_id)
  WITH CHECK (auth.uid() = author_id);

auth.uid() is what Supabase Auth provides (authentication). The USING clause is authorization. Separation of concerns.


The 401 vs 403 split

Two HTTP status codes you’ll see in real systems:

CodeMeaningPlain English
401 UnauthorizedNot authenticated”Who are you? Log in first.”
403 ForbiddenAuthenticated but not authorized”I know who you are; you can’t do this.”

Confusing because 401 is named “Unauthorized” but actually means “Unauthenticated.” Blame the HTTP spec from 1997.

Use them correctly in your APIs:

  • User is not logged in → return 401
  • User is logged in but lacks permission → return 403

This helps the client know whether to redirect to login (401) or show a “you don’t have permission” message (403).


Where authn vs authz lives in your stack

A typical Next.js + Supabase webapp:

ConcernWhereMechanism
Sign upBrowser → Supabase AuthEmail/password, magic link, social
LoginBrowser → Supabase AuthSame
Session refreshServer → Supabase AuthCookie via middleware
”Who is this user?” on requestsServergetServerSession()
Per-row access rulesDatabaseRLS policies
Per-action rulesServer codeIf-checks before sensitive ops
Admin routesServerCheck role in middleware or page
API endpoint protectionServerVerify session + roles

Authentication is mostly handled by the auth service. Authorization is your code + database rules.


Common authorization patterns

Resource ownership

“You can only access things you own.” Most common pattern. RLS handles this naturally.

Team / org membership

“You can access things in your team.” Look up the user’s team_id; filter by it.

Roles

“Admins can do X; editors can do Y; viewers can do Z.” Roles stored in a table, checked at relevant points.

Public / private / unlisted

“Anyone with the link” — soft authorization. Useful for sharing.

Time-bound

“You can edit this draft for 24 hours after creating it.”

Approval flows

“Editor changes are queued for admin approval before publishing.”

Each pattern has implementation choices. For most solo / small-team webapps, ownership + roles + RLS gets you 90% of the way.


Common gotchas

  • Confusing the two. Triple-check whether a bug is authn or authz before chasing it. Different fixes.

  • Client-side authorization only. Hiding the “Delete” button on the frontend without enforcing the rule server-side means anyone who knows the API call can delete. Server-side checks are mandatory.

  • Forgetting authorization in API routes. A /api/posts/[id]/delete that only checks “is logged in” but not “is the author” — anyone can delete anyone’s post. Always check both.

  • Trusting JWT claims for role-checking too far. A JWT might contain role: "admin", but that came from your auth issuance. If your role-assignment is buggy, this is wrong. Re-check from DB for sensitive operations.

  • Using anon key in client + no RLS on table. Anyone with the anon key (everyone) can read the table. Always enable RLS.

  • Mixing service_role and anon keys. Server code can legitimately use service_role to bypass RLS for admin tasks. But that decision must be explicit. Default to using anon key + a user session.

  • Forgetting to remove access on user deletion. Cleanly: cascade deletes, soft delete, archive. Bad: leaving orphaned records the now-deleted user “still owns.”

  • Authorization based on the URL alone. /admin/dashboard blocked but /admin/users not. Authorize at the API/data level, not just routes.

  • Inconsistent rules between layers. Frontend says one thing, backend another. The user sees mixed signals. Single source of truth (RLS in Postgres) helps.

  • Time-of-check, time-of-use bugs. Authorization checked at request start, but the resource changed during the request. Race conditions. Real but advanced.

  • Authorization that depends on client-supplied data. “Delete this post if you’re the author” but the client says “I’m the author of post 123” — never trust the client. Use the authenticated user’s ID, not what the client claims.

  • Long-lived sessions without re-checking permissions. A user demoted from admin yesterday still has admin JWT today. JWT expiry helps; refresh tokens help; in critical systems, re-check on every important action.

  • No audit log for sensitive operations. When something goes wrong (or right) and someone asks “who did X and when?” — no answer. Log access decisions for sensitive ops.

  • “Authentication” used as a synonym for “all auth stuff.” Common in casual speech. Be specific when designing or debugging.

  • Over-engineering for small teams. A side project rarely needs RBAC + ABAC + policy engines. Roles + ownership covers most cases.


See also

Sources