- Published on
Authentication vs Authorization, They're Not the Same Thing
- Authors

- Name
- Alex Peng
- @aJinTonic
Authentication and authorization are two of the most important concepts in application security, and they're regularly conflated, even by experienced developers. The distinction matters because confusing them leads to real vulnerabilities.
The Definitions
Authentication (AuthN): verifying the identity of a requester. "Who are you?"
Authorization (AuthZ): determining what an authenticated requester is allowed to do. "What can you do?"
Authentication comes first. You can't make meaningful authorization decisions until you know who is making the request.
A Concrete Example
GET /api/invoices/INV-456
Authorization: Bearer eyJhbGc...
Authentication step: decode and verify the JWT. Is the token valid? Is it expired? Does the signature check out? If yes, we know the requester is userId: 42.
Authorization step: is userId: 42 allowed to see INV-456? Does invoice 456 belong to their account? Is their subscription tier allowed to access the invoices API?
If you only do authentication, verify the token and extract the userId, but skip authorization, any authenticated user can view any invoice. This is called Broken Object Level Authorization (BOLA), and it's the #1 vulnerability category in the OWASP API Top 10.
Common Authorization Patterns
Role-Based Access Control (RBAC)
Users are assigned roles (admin, editor, viewer). Permissions are attached to roles, not users.
type Role = 'admin' | 'editor' | 'viewer'
const permissions: Record<Role, string[]> = {
admin: ['invoices:read', 'invoices:write', 'invoices:delete', 'users:manage'],
editor: ['invoices:read', 'invoices:write'],
viewer: ['invoices:read'],
}
function can(user: User, permission: string): boolean {
return permissions[user.role]?.includes(permission) ?? false
}
// Usage
if (!can(currentUser, 'invoices:delete')) {
throw new ForbiddenError()
}
RBAC works well for coarse-grained access control: "admins can do everything, editors can write, viewers can only read."
Attribute-Based Access Control (ABAC)
Permissions are based on attributes of the user, the resource, and the environment. More flexible than RBAC, but more complex.
function canAccessInvoice(user: User, invoice: Invoice): boolean {
// User's own invoices
if (invoice.ownerId === user.id) return true
// Admin can see all invoices
if (user.role === 'admin') return true
// Accountants can see invoices for their assigned clients
if (user.role === 'accountant' && user.clientIds.includes(invoice.clientId)) return true
return false
}
Resource-Based Authorization
Each resource stores who has access. Common for collaborative tools (Google Docs, Notion, GitHub repos).
async function canAccessDocument(userId: string, documentId: string): Promise<boolean> {
const share = await db.documentShares.findOne({
documentId,
userId,
})
return !!share
}
Where Developers Get Into Trouble
Missing authorization checks: the most common mistake. You authenticate the user but forget to check if they're allowed to access the specific resource.
// ❌ Only authenticates, no authorization
async function getInvoice(req: Request) {
const invoiceId = req.params.id
return db.invoices.findById(invoiceId)
}
// ✓ Authenticates + authorizes
async function getInvoice(req: Request) {
const invoiceId = req.params.id
const invoice = await db.invoices.findById(invoiceId)
if (invoice.ownerId !== req.user.id && req.user.role !== 'admin') {
throw new ForbiddenError()
}
return invoice
}
Authorization in the wrong layer: checking permissions in the UI is for UX (hiding buttons), not for security. Always enforce authorization on the server.
Returning 404 vs 403: a common question is whether to return 404 Not Found or 403 Forbidden when a user requests a resource they can't access. In most cases, return 404, it doesn't reveal that the resource exists, which prevents information leakage.
Separating AuthN and AuthZ
Clean architecture often separates these into distinct middleware:
// Authentication middleware, runs first, attaches user to request
app.use(authenticate) // throws 401 if token is invalid
// Route handler, does authorization explicitly
app.get('/invoices/:id', async (req, res) => {
const invoice = await getInvoice(req.params.id)
// Authorization, explicit, in code, not hidden in middleware
if (!canAccessInvoice(req.user, invoice)) {
return res.status(404).json({ error: 'Not found' })
}
res.json(invoice)
})
Authentication once, authorization everywhere it's needed.
The Summary
- 401 Unauthorized: authentication failure, we don't know who you are
- 403 Forbidden: authorization failure, we know who you are, but you can't do this
Get the vocabulary right, keep the concerns separate, and always authorize at the resource level, not just at the route level.