State and Identity · capstone
Capstone project: Project: Who May Change the List
Your reading HttpOnly /me reports the session’s user, and every write needs a token that may write. One thing is missing: a session can start, but it can never end. This project adds POST /logout and then checks the whole chapter against the contract.
What the contract asks
Open SERVICE_CONTRACT.md and read its “Who you are” section as a checklist:
POST /loginwith a known user’s Basic credentials: 200,{"user": name}, andset-cookie: session=<id>; HttpOnly; Max-Age=3600; Path=/. Anything else: 401 with the Basic challenge, no cookie, and no session.GET /me: 200, with the session’s user ornull.POST /logout: 204 with no body and nocontent-length; the session ends, andset-cookieexpires the cookie.POST,PUT, andDELETEon books: 401 withwww-authenticate: Bearerfor a missing, malformed, or unknown token, 403 for a token that may only read, checked before the book id and the body.405 with
allow: POSTon/loginand/logout, andallow: GET, HEADon/me.No reply body contains a password, a token, or a session id.
Every row except logout should already hold. Run the helper’s who scenario to see where you stand:
python pl_http.py who
It logs in as ana with Basic credentials, reads /me with the cookie it got, sends POST /logout with that cookie and Content-Length: 0, reads /me again with the same old cookie, and last sends POST /books with the reading token. Today the logout answers HTTP/1.0 404 Not Found, and the second /me still answers {"user": "ana"}: the session is alive.
Ending a session
Logging out takes two steps, and they are not equally important.
First, the server removes the session from state["sessions"]. This is the step that counts. Once the id is gone, the cookie is worthless: any request that carries it, from this client or from anyone who copied it, gets {"user": null}.
Second, the reply asks the client to forget the cookie, with a set-cookie whose Max-Age is 0. Your session_cookie already writes that: session_cookie("", max_age=0) gives session=""; HttpOnly; Max-Age=0; Path=/ when it uses SimpleCookie, which writes the empty "". An session=; HttpOnly; Max-Age=0; Path=/. Either line expires the cookie. A jar that reads this line drops its session cookie at once. But Lesson 1 said it: every attribute is a request the client may ignore. A server that only sent this line and kept the session would leave the old cookie working for anyone who kept a copy.
To find the session, read the session cookie the same way session_user does, and remove it with state["sessions"].pop(value, None). With a default, pop returns it instead of raising
Milestones
In
handle, add a branch for/logout. APOSTends the session its cookie names, if any, and answers 204 with an empty body, nocontent-length, and aset-cookieheader fromsession_cookie("", max_age=0). Any othergets not_allowed("POST").Run
python pl_http.py whoagain. The logout now answersHTTP/1.0 204 No Content, and the next/mewith the old cookie answers{"user": null}. The reading token’sPOST /booksstill getsHTTP/1.0 403 Forbidden. Under the 204, the helper prints the line that expires the cookie, here asSimpleCookiewrites it:
set-cookie: session=""; HttpOnly; Max-Age=0; Path=/
You can add a test of logout to
tests/test_server.py.make_requesttakes aof headers, so a test can send a cookie, for example:
def test_logout_ends_the_session():
state = reading_server.sample_state()
session_id = reading_server.start_session(state, "ana")
cookie = {"cookie": "session=" + session_id}
response = reading_server.handle(make_request("POST", "/logout", headers=cookie), state)
assert response["status"] == 204
me = reading_server.handle(make_request("GET", "/me", headers=cookie), state)
assert json.loads(me["body"].decode("utf-8")) == {"user": None}
Run
python -m pytest -q. Every test should pass, the tests that came with the chapter included.
python -m pytest -q
What gets checked
Check my work checks the whole chapter again with users, passwords, tokens, and books it has not shown you, and with session ids it chooses itself, as Lesson 2’s check did. It calls handle and the helpers directly for logins, /me, logout, and every kind of write, compares the state before and after each refusal, and searches every reply for the credentials it sent. It builds your server’s handler class with make_server without opening a set-cookie and www-authenticate lines that come back. Last, it runs your tests once, as they are; they must all pass, and none may import socket. The who scenario on your
Then press Check my work. Your list now says who may change it. Every reader, though, still downloads the whole list every time they ask, even when nothing has changed since the last time. Chapter 8 teaches the server to say “nothing changed” and the client to remember what it already has.
Task
Add POST /logout to reading_server.py, and make every rule of “Who you are” in SERVICE_CONTRACT.md hold.
POST /logoutremoves the session itssessionnames from state["sessions"], if there is one, and answers 204 with the bodyb"", nocontent-length, and aset-cookieheader fromsession_cookie("", max_age=0). Without a cookie, or for a session already ended, it still answers 204. Any otheron /logoutgets 405 withallow: POST.A client’s
POST /logoutsends no body, so, like everyPOSTto this server, it sendscontent-length: 0; without it,read_bodyanswers 411.Everything from Lessons 2, 5, and 6 still holds: Basic login with a session in an
HttpOnlycookie or 401 with the Basic challenge,/mewith the user ornull, bearer-protected writes with 401 or 403 by cause before the book id and the body, the 405s, and no password, token, or session id in any reply.
Run python pl_http.py who in the python -m pytest -q to run your tests.
Then press Check my work. With users, tokens, books, and session ids you have not seen, it calls handle and the helpers directly for logins, /me, logout, and every kind of write, comparing the state before and after each refusal and searching each reply for credentials. It builds your server’s handler class with make_server without opening a socket. Nothing from your terminal is graded.