Build the mental model
This project combines the cookies-and-sessions lesson with authentication versus authorization. The simulation is in-memory only and is not a real login-system template — per the password-security lesson, use bcrypt/Argon2 or Auth0/Clerk in a real project.
Because HTTP is stateless, the server creates a session record after login and hands the browser a cookie holding the session ID, which the browser auto-attaches to every later request.
| Concept | Answers the question |
|---|---|
| Authentication | Who is this? Happens once at login, then implicitly on every successful session lookup |
| Authorization | Is this identified user allowed to do this specific thing? Checked per request/action |
Not a production auth template
This code exists only to show the concept, and skips password hashing, cookie security flags (HttpOnly, Secure, SameSite), and session expiry — use an established library or provider for a real system.
Watching both requests side by side is the point: the admin action fails not because the session or cookie is broken, but because authorization rules say so.
AUTHENTICATION VS AUTHORIZATION FLOW
------------------------------------
AUTH FLOW: LOGIN -> SESSION -> COOKIE -> AUTHORIZATION CHECK
----------------------------------------------------------------
[User] --1. login(username, password)--> [Backend]
|
2. verify credentials
|
3. create session record
(id -> user, role)
|
4. Set-Cookie: session_id=X
v
[Browser stores cookie]
|
(cookie is attached automatically to later requests)
v
[Request N] --Cookie: session_id=X--> [Backend]
|
5. session lookup (authN)
|
6. authorization check (authZ)
|
+------------------+------------------+
| |
allowed denied
v v
200 OK response 403 ForbiddenConnect it to a real scenario
Read the code before running it and predict the output: alice (a normal user) and bob (an admin) both log in and then attempt a 'delete-user' action.
Alice logs in
Backend verifies her password, creates a session record, and returns a cookie-like string carrying the session ID.
Request 1: view-dashboard
Using alice's cookie, the session lookup succeeds and the action is allowed.
Request 2: delete-user (denied)
Alice is still authenticated, but her role is 'user', so the authorization check denies the action with a 403.
Bob logs in
A new session is created for bob, whose role is 'admin', with its own session ID.
Request 3: delete-user (allowed)
Bob's session passes both the authentication and authorization checks, so the action is allowed.
Run the code, then change bob's role to 'user' and rerun it to confirm the denial logic reacts correctly.
Try the working example
// Mock user 'database'. In a real system, passwords are never stored or
// compared in plaintext -- see the password-security lesson and use an
// established library (bcrypt, Argon2) or an auth provider (Auth0, Clerk).
const users = {
alice: { password: 'hunter2', role: 'user' },
bob: { password: 'adminpass', role: 'admin' },
};
// In-memory session store: sessionId -> { username, role }
const sessions = {};
let sessionCounter = 0;
function createSessionId() {
sessionCounter += 1;
return `sess_${sessionCounter}`;
}
function login(username, password) {
const user = users[username];
if (!user || user.password !== password) {
return { ok: false, reason: 'invalid credentials' };
}
const sessionId = createSessionId();
sessions[sessionId] = { username, role: user.role };
return { ok: true, cookie: `session_id=${sessionId}` };
}
function request(cookie, action) {
const sessionId = cookie.split('=')[1];
const session = sessions[sessionId];
if (!session) {
return { status: 401, body: 'not authenticated' };
}
// Authentication (who are you) already passed. This is authorization
// (are you allowed to do THIS specific thing).
if (action === 'view-dashboard') {
return { status: 200, body: `${session.username} viewed the dashboard` };
}
if (action === 'delete-user') {
if (session.role !== 'admin') {
return {
status: 403,
body: `${session.username} is authenticated but NOT authorized (needs admin)`,
};
}
return { status: 200, body: `${session.username} deleted a user` };
}
return { status: 404, body: 'unknown action' };
}
const aliceLogin = login('alice', 'hunter2');
console.log('Login (alice):', aliceLogin);
console.log('Request 1 (view-dashboard):', request(aliceLogin.cookie, 'view-dashboard'));
console.log('Request 2 (delete-user): ', request(aliceLogin.cookie, 'delete-user'));
const bobLogin = login('bob', 'adminpass');
console.log('Login (bob): ', bobLogin);
console.log('Request 3 (delete-user): ', request(bobLogin.cookie, 'delete-user'));Running the code makes authentication success (login, view-dashboard) and authorization denial (delete-user) concrete:
Login (alice): { ok: true, cookie: 'session_id=sess_1' }
Request 1 (view-dashboard): { status: 200, body: 'alice viewed the dashboard' }
Request 2 (delete-user): { status: 403, body: 'alice is authenticated but NOT authorized (needs admin)' }
Login (bob): { ok: true, cookie: 'session_id=sess_2' }
Request 3 (delete-user): { status: 200, body: 'bob deleted a user' }5-minute try-it
Add a third user with role 'moderator' who is allowed to view-dashboard and a new 'edit-post' action but not delete-user, and extend the authorization check to prove authentication and authorization are still independently checked.
One important caution
Using predictable session IDs (sess_1, sess_2...) like this demo in production — real session IDs must be cryptographically random
Conflating a 403 (authorization denial) with a 401 (not authenticated) as if they were the same kind of failure
OWASP Session Management Cheat Sheet — How the Web Works