Sessions, 401 and 403 · Backend Development
How does a server know who you are, and what you're allowed to do?
Course contentsWhy you need it
"Broken access control" is number one in the OWASP Top 10 (2021), the best-known list of web security risks: users reading or changing things that belong to someone else. It usually isn't clever hacking, just a missing check in one handler. Learning to ask two questions on every request, who is this? and may they do this?, prevents most of it.
The intuition
Two different questions, two different status codes:
- Authentication: who are you? No valid login → 401 Unauthorized.
- Authorization: you're logged in, but may you do this? No → 403 Forbidden.
A common way to remember a login is a session:
- The user logs in with a password. The server checks it and creates a random session id, stored server-side:
SESSIONS["s-8f3a…"] = "ada". - The response sets a cookie:
Set-Cookie: session=s-8f3a…; HttpOnly; Secure. - The browser sends it back on every request:
Cookie: session=s-8f3a…. The server looks it up to find the user.
Then each handler checks the rules: an admin may delete any post; a member only their own.
Never take the user's identity from the request body or URL ({"user": "ada"}): anyone can type that. Only trust what the server itself stored.
Definition
Authentication
Finding out who is making the request. Failing it means 401.
Authorization
Deciding whether that user may do this action on this thing. Failing it means 403.
Session
A random id the server stores next to a user, sent to the browser in a cookie and returned on every request.
Examples
Who is this?
Try it yourselfRead the session cookie and look it up.
SESSIONS = {"s-ada": "ada", "s-bo": "bo"}
ROLES = {"ada": "admin", "bo": "member"}
POSTS = {1: {"author": "bo", "text": "Hello"}, 2: {"author": "ada", "text": "Welcome"}}
def current_user(headers):
cookie = headers.get("Cookie", "")
for part in cookie.split(";"):
name, _, value = part.strip().partition("=")
if name == "session" and value in SESSIONS:
return SESSIONS[value]
return None
print(current_user({"Cookie": "theme=dark; session=s-bo"}))
print(current_user({"Cookie": "session=s-fake"}))
print(current_user({}))bo None None
- L6No Cookie header means an empty string, not an error.
- L8"session=s-bo" splits into name "session" and value "s-bo".
- L9Only ids the server created count.
- L14An unknown id: nobody.
None
401 versus 403
Try it yourselfCheck who, then check what.
SESSIONS = {"s-ada": "ada", "s-bo": "bo"}
ROLES = {"ada": "admin", "bo": "member"}
POSTS = {1: {"author": "bo", "text": "Hello"}, 2: {"author": "ada", "text": "Welcome"}}
def current_user(headers):
cookie = headers.get("Cookie", "")
for part in cookie.split(";"):
name, _, value = part.strip().partition("=")
if name == "session" and value in SESSIONS:
return SESSIONS[value]
return None
def edit_post(headers, post_id, text):
user = current_user(headers)
if user is None:
return 401, {"error": "log in first"}
post = POSTS[post_id]
if user != post["author"] and ROLES[user] != "admin":
return 403, {"error": "not your post"}
post["text"] = text
return 200, post
print(edit_post({}, 1, "Hi"))
print(edit_post({"Cookie": "session=s-bo"}, 2, "Hi"))
print(edit_post({"Cookie": "session=s-bo"}, 1, "Hi"))
print(edit_post({"Cookie": "session=s-ada"}, 1, "Edited"))(401, {'error': 'log in first'})
(403, {'error': 'not your post'})
(200, {'author': 'bo', 'text': 'Hi'})
(200, {'author': 'bo', 'text': 'Edited'})
- L16No login: 401.
- L19Logged in as bo, but post 2 is ada's and bo isn't an admin: 403.
- L26ada is an admin, so she may edit anyone's post.
Common mistakes
Trusting the client about identity
A user id in the body, URL or a plain cookie like user=ada can be changed by anyone. Look the user up from a session (or a signed token) on the server.
Checking only that someone is logged in
Being logged in doesn't mean every post is yours. Check ownership or role in every handler that changes data.
Mixing up 401 and 403
401 means "log in" (the client can fix it by authenticating). 403 means "no, even though we know who you are".
Quick quiz
1. Bo is logged in and tries to delete Ada's account. Bo isn't an admin. Which status?
Show the answer
Answer: A
We know who Bo is (authenticated), but he isn't allowed (not authorized).
2. Where should a handler get the current user from?
Show the answer
Answer: A
Only data the server stored itself can be trusted; the rest is typed by the client.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictFour requests
Type exactly what this prints.
- ModifyOnly your own posts
Add the missing authorization check to
delete_post: the author or an admin may delete a post; anyone else gets403{"error": "not your post"}. Keep 401 and 404 as they are. - WriteAn access policy
Write
check(user, action, doc)that returns the status code for a request.userisNoneor a dict like{"name": "bo", "role": "member"};docis like{"owner": "ada", "public": True};actionis"read","edit"or"delete".Rules, in order:
- reading a public doc is allowed for everyone, even without login →
200 - no user →
401 - admins may do anything →
200 - the owner may read, edit and delete →
200 - other members may read (it's not public, so no) →
403for everything
- reading a public doc is allowed for everyone, even without login →
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| Authentication | 认证 | Who are you? Failing it: 401. |
| Authorization | 授权 | May you do this? Failing it: 403. |
| Session | 会话 | A server-side record of a login, found through a cookie. |
| Cookie | Cookie | A small value the browser stores and sends back to the same site. |