Build the mental model
Authentication and authorization sound alike, but they answer two completely different questions.
Authentication answers "who are you?" — proving identity with a password, a passkey, or a valid session token.
Authorization answers a separate question: what is this identity allowed to do, right now, on this specific resource? Passing one never automatically grants the other.
Consider GET /users/123. Being logged in does not automatically entitle a user to view every user's data.
The insecure-direct-object bug
If a logged-in user can change the ID in a URL and see someone else's private data, authentication worked perfectly — but authorization failed.
Treat them as two separate gates, checked in order. Authentication confirms identity once; authorization must be re-evaluated per resource and per action.
- Authentication
- The process of verifying who a user is, typically by checking credentials such as a password, passkey, or valid session token.
- Authorization
- The process of deciding what an already-identified user is permitted to do, checked per resource and per action.
AUTHENTICATION VS AUTHORIZATION
-------------------------------
REQUEST
|
v
[AUTHENTICATION] <- who are you?
|
| pass (identity confirmed)
v
[AUTHORIZATION] <- allowed to do THIS, on THIS resource?
|
+-- pass --> ALLOW (proceed)
|
+-- fail --> DENY (403, even though logged in)Connect it to a real scenario
The function below models the two-gate flow as real, runnable code, checking each gate separately instead of collapsing them into one boolean.
If authentication fails, the function stops immediately and returns DENY without even looking at authorization.
Case 1 — wrong owner
User 456 requests the resource owned by user 123. Authentication passes, authorization correctly fails: DENY.
Case 2 — correct owner
The same user requests their own resource. Both gates pass: ALLOW.
Keeping the two checks structurally separate, with separate results in the output, is what catches this bug before production.
Try the working example
function checkAccess(request) {
const authResult = request.isAuthenticated
? { gate: "authentication", passed: true }
: { gate: "authentication", passed: false, reason: "not logged in" };
if (!authResult.passed) {
return { authentication: authResult, authorization: null, decision: "DENY" };
}
const isOwner = request.requestedResourceOwnerId === request.actualUserId;
const authzResult = isOwner
? { gate: "authorization", passed: true }
: {
gate: "authorization",
passed: false,
reason: `user ${request.actualUserId} does not own resource owned by ${request.requestedResourceOwnerId}`,
};
return {
authentication: authResult,
authorization: authzResult,
decision: authzResult.passed ? "ALLOW" : "DENY",
};
}
const authenticatedButNotOwner = checkAccess({
isAuthenticated: true,
requestedResourceOwnerId: 123,
actualUserId: 456,
});
const fullyAuthorized = checkAccess({
isAuthenticated: true,
requestedResourceOwnerId: 123,
actualUserId: 123,
});
console.log("Case 1 - authenticated but not the owner:");
console.log(JSON.stringify(authenticatedButNotOwner, null, 2));
console.log("\nCase 2 - authenticated and owns the resource:");
console.log(JSON.stringify(fullyAuthorized, null, 2));Case 1 - authenticated but not the owner:
{
"authentication": {
"gate": "authentication",
"passed": true
},
"authorization": {
"gate": "authorization",
"passed": false,
"reason": "user 456 does not own resource owned by 123"
},
"decision": "DENY"
}
Case 2 - authenticated and owns the resource:
{
"authentication": {
"gate": "authentication",
"passed": true
},
"authorization": {
"gate": "authorization",
"passed": true
},
"decision": "ALLOW"
}5-minute try-it
Extend checkAccess to also handle a third role, "admin", which is authorized for any resource regardless of ownership. Add a role field to the request object and update the authorization check accordingly, then verify an admin can access a resource they don't own while a regular user still cannot.
One important caution
Checking only "is this user logged in" and assuming that covers access control for every action they take.
Trusting a resource ID from the client (URL, body, query string) without verifying the authenticated user actually owns or may access it.
Check your understanding
OWASP Authorization Cheat Sheet — Digital Privacy & Modern Security