CORS · practice
Cookies Across Origins
The front end can read the book
POST /tokens gives a token to a logged-in reading.example. So the front end’s page must send a request to another
A fetch leaves cookies at home unless asked
In Chapter 8 every request considered the jar. That was right for those requests: a form, a link, and a plain image always do. A script’s fetch does so only for its own origin. For another origin it leaves every cookie at home, unless the page asks for credentials. In the model that is "credentials": True in the wanted
attaches_cookies(kind, url, page_origin, *, credentials) states that rule: True for every kind but "fetch", True for a fetch to the page’s own origin, and otherwise whatever credentials says. It answers whether cookies are considered at all. Which cookies then go is still the business of cookies_to_send.
A script on https://app.reading.example fetches https://reading.example/me and asks for nothing special. Does the session cookie go along?
A stricter permission
A response to a request that carried a cookie may hold what only the logged-in reader should see. So a browser asks more of the server before a page may read it:
Access-Control-Allow-Originmust name the page’s exact origin. A*is not accepted.The response must also say
Access-Control-Allow-Credentials: true, in exactly that spelling.
may_read gets this rule in place of the return False of Lesson 2. The same holds for a preflight that comes before a request with credentials. preflight_allows gets a keyword-only credentials=False. It hands the may_read, and when it is true, a * in the list of
On the server, cors_headers gets the same keyword. With credentials=True, an origin that is allowed also gets access-control-allow-credentials: true. An origin that is not allowed gets neither header.
A response to a fetch with credentials carries Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true. May the page read it?
Follow the token
Here is the front end’s POST /tokens, one decision at a time.
The request has no body and no header of the page’s own. It is a simple request, so no preflight is sent.
The page asked for credentials, so
attaches_cookiessays yes. The session cookie isSameSite=Lax, and the two hosts are one site, socookies_to_sendlets it go.The browser adds
Origin: https://app.reading.example. Your server checks that header on this route since Chapter 8, and today it would refuse. The front end’s origin has to joinstate["trusted_origins"].The server makes a token.
cors_headers, withcredentials=True, names the front end’s origin and allows credentials.may_readfinds the exact origin andtrue, and the page reads its token.
A page on notes.reading.example can send the same POST. It is on the same site and may ask for credentials too, so ana’s cookie goes along. The Origin check refuses it with 403 before any token is made. And the page could not have read an answer anyway, because no response names its origin.
A page on notes.reading.example makes the same POST /tokens with credentials. What stops it from getting a token?
One set
After step 3 your state would hold the same decision twice: cors_origins for reading, trusted_origins for the Origin check. Two sets that must agree will one day disagree. Delete cors_origins from sample_state(), put the front end’s origin into trusted_origins, and pass that one set to every call of cors_headers.
The service’s own origin is now in the set that
The front end has gained something too. Every POST that the Origin check guards now accepts it, so its page can send POST /logout and log ana out. An origin you let read with credentials is an origin you trust with the session.
Trust has a limit. The front end still cannot post the delete form: POST /books/delete asks for the token from the reading list’s own page, and the front end’s page does not have it.
Only two routes answer with credentials: GET /me and POST /tokens. The book routes need none. A bearer token that a script puts in a header is not a credential in the browser’s sense, and no cookie is read there.
Read the rest of visit.py
Open visit.py and find the block marked Chapter 9. Its nine lines call four visit() carries a fetch to another origin from start to finish.
Your task
In mini_browser.py: write attaches_cookies, and grow may_read and preflight_allows.
In reading_server.py: add the keyword to cors_headers, make the change to sample_state(), and pass state["trusted_origins"] in the three calls you have. Then add one line to the "/me" branch, in the case for GET and HEAD, and the same line to the "/tokens" branch, in the case for POST:
response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True))
Then watch the whole exchange on
python -m pytest -q
python pl_sec.py cors
The tests end in 37 passed. The scenario plays the front end, then the neighbor, then another site. This is the front end’s first request and the neighbor’s last:
--- the front end, https://app.reading.example: POST /tokens, and it asks for credentials ---
sent: POST /tokens cookie: yes token: no origin: https://app.reading.example
your server answered 200
access-control-allow-origin: https://app.reading.example
access-control-allow-credentials: true
vary: Origin
the page may read the response: yes
the page now holds a token (not shown)
sent: GET /me cookie: yes token: no origin: https://notes.reading.example
your server answered 200
vary: Origin
the page may read the response: no (the response did not give this origin permission to read it)
The last line of the result says the front end read 4 of 4 responses; pages on other origins read 0 of 4; the request without an Origin header was answered 200.
The model has cookies only. In the standard, credentials also covers a login that the browser remembers for SameSite=None, which is a larger decision than this lesson makes. Even then a browser may withhold the cookie. Safari blocks cookies that go to a third party, Firefox keeps a separate cookie jar for each site, and Chrome sends them unless the user has turned that off or is in an Incognito window.
Then press Check my work.
Task
Teach the model when a fetch carries
In mini_browser.py:
attaches_cookies(kind, url, page_origin, *, credentials)returnsTruewhenkindis not"fetch",Truewhensame_origin(url, page_origin), and otherwise theof credentials.may_read(response, page_origin, *, credentials): without credentials, as in Lesson 2. With credentials, it returnsTrueonly whenaccess-control-allow-originequalspage_originandaccess-control-allow-credentialsis exactly"true". A*gives no permission then.preflight_allows(response, page_origin, method, headers, *, credentials=False)handscredentialson tomay_read. Whencredentialsis true, a*inaccess-control-allow-methodsor inaccess-control-allow-headerscounts for nothing.
In reading_server.py:
cors_headers(request, trusted, *, credentials=False): for anthat is allowed, and only then, credentials=Trueadds"access-control-allow-credentials": "true".In
sample_state(),"trusted_origins"holds"https://reading.example"and"https://app.reading.example", and"cors_origins"is gone. The three calls ofcors_headersfrom Lessons 2 and 4 passstate["trusted_origins"].In the
"/me"branch, in the case for GET and HEAD, and in the"/tokens"branch, in the case for POST:response = add_headers(response, cors_headers(request, state["trusted_origins"], credentials=True)). No other route allows credentials.
Run python -m pytest -q, python mini_browser.py, and python pl_sec.py cors.
Then press Check my work. It calls attaches_cookies with ten requests you have not seen: a form, a link, an image, and fetches to the page’s own origin, to another origin, and to another may_read and preflight_allows with answers of its own: with and without the credentials header, with a star in each place, and with other spellings of true. It calls cors_headers with credentials=True for origins inside and outside the set. Then it uses a state of its own that has no cors_origins, logs a user in, and sends requests to handle(): /me and /tokens from the front end’s origin and from two others, and nine other routes, none of which may allow credentials or answer a star. A browser model of its own plays the front end, a neighbor, and another site against your server. It also reads sample_state()["trusted_origins"]. Nothing from your