The short version
Authentication asks "who are you?" Authorization asks "what are you allowed to do?"
Authentication is the login screen. Authorization is the check that happens after login, on every single request, to decide whether you can view that page, edit that record, or delete that file.
Most vibe coded apps nail authentication and completely ignore authorization. That's how they get hacked.
The real world analogy
You walk into an office building. The front desk checks your ID and gives you a visitor badge. That's authentication. They've confirmed your identity.
But the badge doesn't mean you can walk into the server room, open the CEO's filing cabinet, or sit in on a board meeting. The doors you can open and the rooms you can enter depend on your authorization level.
A vibe coded app is like a building where the front desk checks your ID, then hands you a master key. You proved who you are, so clearly you should have access to everything.
How the confusion happens
The root cause is naming. Developers install "NextAuth" or "Supabase Auth" and think they've handled "auth." But these libraries handle authentication only:
// This is AUTHENTICATION - checking who the user is
const session = await getServerSession(authOptions);
if (!session) {
return Response.json({ error: 'Not logged in' }, { status: 401 });
}
// Everything below this line is an authorization problem
// that your "auth" library doesn't solve
After that session check, you know the user is logged in. You know their ID, email, maybe their role. But you haven't checked whether they should be able to perform the specific action they're requesting.
The gap that gets apps hacked
Here's a typical vibe coded API route:
// Authentication: present
// Authorization: missing
export async function DELETE(request, { params }) {
const session = await getServerSession();
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Deletes ANY project by ID - no check on ownership
await db.project.delete({ where: { id: params.id } });
return Response.json({ success: true });
}
Any logged in user can delete any project. The developer checked authentication ("is someone logged in?") but skipped authorization ("is this person allowed to delete this specific project?").
The fix requires one more check:
export async function DELETE(request, { params }) {
const session = await getServerSession();
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Authorization: verify ownership
const project = await db.project.findUnique({ where: { id: params.id } });
if (!project || project.ownerId !== session.user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
await db.project.delete({ where: { id: params.id } });
return Response.json({ success: true });
}
Notice the different HTTP status codes. 401 Unauthorized actually means "not authenticated" (the naming is confusing, blame the HTTP spec). 403 Forbidden means "authenticated but not authorized."
Authentication patterns
Authentication is mostly a solved problem. Use an established solution:
- Supabase Auth / Firebase Auth: Handle signup, login, OAuth, email verification, and token management. Good for most apps.
- NextAuth (Auth.js): Flexible authentication for Next.js with many providers.
- Clerk / Auth0: Managed services that handle the entire authentication flow including UI components.
Don't build your own authentication. It involves password hashing, token management, session handling, CSRF protection, rate limiting on login, and secure password reset flows. There are too many ways to get it wrong.
Authorization patterns
Authorization is the part you have to build yourself, because it depends on your application's specific rules. Common patterns:
Role based access control (RBAC)
Users have roles. Roles have permissions.
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
};
function authorize(userRole, requiredPermission) {
return PERMISSIONS[userRole]?.includes(requiredPermission) || false;
}
Resource based access control
Permissions depend on the relationship between the user and the specific resource.
// Can this user edit this document?
async function canEdit(userId, documentId) {
const doc = await db.document.findUnique({ where: { id: documentId } });
return doc.ownerId === userId || doc.collaborators.includes(userId);
}
Row Level Security (database level)
Supabase RLS enforces authorization at the database layer:
-- Users can only update their own profiles
CREATE POLICY "Users update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
This is powerful because even if your API code has a bug, the database won't allow unauthorized access.
The checklist
When you build a feature, ask yourself two separate questions:
- Authentication: Does this endpoint verify the user's identity? Is the session/token validated server side?
- Authorization: Does this endpoint check that this specific user has permission to perform this specific action on this specific resource?
If you only answer question 1, your app is half secured. And half secured means not secured at all.
Check your site
Want to know if your app has gaps between authentication and authorization? Scan it now and find out in 60 seconds.
Frequently Asked Questions
- What is the difference between authentication and authorization?
- Authentication (authn) verifies identity: are you who you claim to be? Authorization (authz) verifies permissions: are you allowed to do what you're trying to do? Authentication happens first, authorization happens after.
- Why do developers confuse authentication and authorization?
- Because auth libraries like NextAuth, Supabase Auth, and Firebase Auth handle authentication well but barely touch authorization. Developers see 'auth' in the name and assume their app is secured, when they've only solved half the problem.
- Can I have authorization without authentication?
- Technically yes, in cases like rate limiting by IP or serving different content by region. But for most applications, authorization depends on knowing who the user is first. You can't check if User 42 is allowed to delete an invoice without first confirming the request came from User 42.
Your AI writes the code. We find what it missed.
Paste your URL. Security audit in 60 seconds.
Scan my app