TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

CORS is not authentication

Continue in TT Lab

Goal

You verify the allow matrix of origins, methods, and headers with real preflight requests.

Why it matters

When the frontend could not read the response, someone allowed an asterisk for every origin. For requests that send cookies, the policy became more complicated, and developers misunderstood that turning on CORS alone would block external requests. In this lab we separate the browser's read policy from server authentication. We do not build the authentication feature for you.

Steps

  1. In /root/work/fa-cors-policy-lab/service.py, origin(value) returns the input string if it is an http or https URL that has a host and has no path, query, fragment, or user information. Otherwise it is ValueError. A trailing / is also a path, so it is rejected.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/fa-cors-policy-lab
test -e /root/work/fa-cors-policy-lab/service.py || cp /opt/fixtures/ten_labs/fa-cors-policy-lab/service.py /root/work/fa-cors-policy-lab/service.py
cd /root/work/fa-cors-policy-lab
  1. In /root/work/fa-cors-policy-lab/service.py, origins(values) is a new list that validates each item with origin and then removes duplicates, keeping the order of first appearance.

  2. In /root/work/fa-cors-policy-lab/service.py, methods(values) allows only GET, POST, PUT, DELETE, and OPTIONS, converts them to uppercase, and removes duplicates. An empty list or any other value is ValueError.

  3. In /root/work/fa-cors-policy-lab/service.py, policy(allowed, credentials) checks that credentials is a bool. If allowed contains '*', it is ValueError, and otherwise it returns {allow_origins:origins(allowed), allow_credentials:credentials}.

  4. In /root/work/fa-cors-policy-lab/service.py, create_app(allowed, credentials=True) is an app that validates the policy and configures CORSMiddleware. It allows only GET and POST, allows the Content-Type and X-Request-ID request headers, and exposes the X-Trace response header. GET /data returns {ok:True} with X-Trace='trace-1'.

  5. In /root/work/fa-cors-policy-lab/service.py, preflight_headers(source, method, requested='X-Request-ID') is a dictionary with three keys: Origin, Access-Control-Request-Method, and Access-Control-Request-Headers. method is uppercase.

  6. In /root/work/fa-cors-policy-lab/service.py, preflight_status(app, source, method, requested='X-Request-ID') sends an OPTIONS request to /data with TestClient and returns the HTTP status. A different origin, DELETE, and an X-Secret header must each give 400.

  7. In /root/work/fa-cors-policy-lab/service.py, cors_observation(app, source) sends GET /data and returns (status, the Access-Control-Allow-Origin value or None, the JSON body). Even for an origin that is not allowed, the 200 body still runs, but the allow-origin header must be absent.

Notes

Validate the origin format

In /root/work/fa-cors-policy-lab/service.py, origin(value) returns the input string if it is an http or https URL that has a host and has no path, query, fragment, or user information. Otherwise it is ValueError. A trailing / is also a path, so it is rejected.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/fa-cors-policy-lab
test -e /root/work/fa-cors-policy-lab/service.py || cp /opt/fixtures/ten_labs/fa-cors-policy-lab/service.py /root/work/fa-cors-policy-lab/service.py
cd /root/work/fa-cors-policy-lab

If you allow a whole URL as an origin, you can confuse a path or user information with the origin.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/01-contract.sh.

Remove duplicate origins

In /root/work/fa-cors-policy-lab/service.py, origins(values) is a new list that validates each item with origin and then removes duplicates, keeping the order of first appearance.

The allow list is a list of exact origins, not a substring match on strings.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/02-contract.sh.

Limit the methods to an allow list

In /root/work/fa-cors-policy-lab/service.py, methods(values) allows only GET, POST, PUT, DELETE, and OPTIONS, converts them to uppercase, and removes duplicates. An empty list or any other value is ValueError.

Do not quietly add PATCH or arbitrary methods that were not allowed.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/03-contract.sh.

Do not allow credentials together with an asterisk

In /root/work/fa-cors-policy-lab/service.py, policy(allowed, credentials) checks that credentials is a bool. If allowed contains '*', it is ValueError, and otherwise it returns {allow_origins:origins(allowed), allow_credentials:credentials}.

The explicit policy of this lab does not accept an asterisk regardless of whether credentials are allowed.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/04-contract.sh.

Attach the real CORS middleware

In /root/work/fa-cors-policy-lab/service.py, create_app(allowed, credentials=True) is an app that validates the policy and configures CORSMiddleware. It allows only GET and POST, allows the Content-Type and X-Request-ID request headers, and exposes the X-Trace response header. GET /data returns {ok:True} with X-Trace='trace-1'.

If you attach headers by hand separately to the preflight and to the real response, the two policies easily drift apart.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/05-contract.sh.

Build the preflight request

In /root/work/fa-cors-policy-lab/service.py, preflight_headers(source, method, requested='X-Request-ID') is a dictionary with three keys: Origin, Access-Control-Request-Method, and Access-Control-Request-Headers. method is uppercase.

The method of the actual request is OPTIONS, and the method you want to check is in a separate header.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/06-contract.sh.

Compute the rejection matrix

In /root/work/fa-cors-policy-lab/service.py, preflight_status(app, source, method, requested='X-Request-ID') sends an OPTIONS request to /data with TestClient and returns the HTTP status. A different origin, DELETE, and an X-Secret header must each give 400.

Do not mix the three kinds of rejection reasons into one request, or you cannot find the missing policy.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/07-contract.sh.

Observe the difference between CORS and authentication

In /root/work/fa-cors-policy-lab/service.py, cors_observation(app, source) sends GET /data and returns (status, the Access-Control-Allow-Origin value or None, the JSON body). Even for an origin that is not allowed, the 200 body still runs, but the allow-origin header must be absent.

curl and server-to-server requests do not follow the browser's CORS read restriction.

After saving, check with bash /opt/lab/checks/fa-cors-policy-lab/08-contract.sh.