TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

CORS is not authentication: design principles

Continue in TT Lab

Summary

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

Why this 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.

How it works

An origin is a combination of scheme, host, and port. If the input settings contain a path or credentials, reject them. Compare the allowed origins exactly, and forbid the wildcard when credentials are allowed. Specify the allowed methods and request headers explicitly in CORSMiddleware. Reproduce both the case where an OPTIONS preflight succeeds and the cases where it fails because of the origin, the method, or the headers.

Origin + 요청 메서드 + 요청 헤더 → OPTIONS 정책 확인
허용: 출처/자격 헤더 제공 → 브라우저가 실제 요청
거절: 읽기 권한 없음 ≠ 서버 인증

A worksheet for reading the contract and predicting failures

What follows is not an answer key to memorize an implementation but a step-by-step code review. Each change fragment deliberately breaks the contract. Note that the normal case may still pass after the change. Before running it, predict which input, exception, or state you would observe to expose the difference, and after implementing it, compare that prediction with the result.

1. Validate the origin format

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.

Basis for the judgment: if you allow a whole URL as an origin, you can confuse a path or user information with the origin.

Faulty change fragment to review:

or url.query

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

2. Remove duplicate origins

origins(values) is a new list that validates each item with origin and then removes duplicates, keeping the order of first appearance.

Basis for the judgment: the allow list is a list of exact origins, not a substring match on strings.

Faulty change fragment to review:

[origin(value) for value in values]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

3. Limit the methods to an allow list

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.

Basis for the judgment: do not quietly add PATCH or arbitrary methods that were not allowed.

Faulty change fragment to review:

"DELETE","OPTIONS","PATCH"

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

4. Do not allow credentials together with an asterisk

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}.

Basis for the judgment: the explicit policy of this lab does not accept an asterisk regardless of whether credentials are allowed.

Faulty change fragment to review:

not isinstance(credentials, (bool, int))

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

5. Attach the real CORS middleware

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'.

Basis for the judgment: if you attach headers by hand separately to the preflight and to the real response, the two policies easily drift apart.

Faulty change fragment to review:

expose_headers=[]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

6. Build the preflight request

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.

Basis for the judgment: the method of the actual request is OPTIONS, and the method you want to check is in a separate header.

Faulty change fragment to review:

method.lower()

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

7. Compute the rejection matrix

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.

Basis for the judgment: do not mix the three kinds of rejection reasons into one request, or you cannot find the missing policy.

Faulty change fragment to review:

client.get("/data",

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

8. Observe the difference between CORS and authentication

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.

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

Faulty change fragment to review:

source

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

What it looks like in the field

TestClient is not a browser. It checks CORS response headers and preflights, but it does not implement the browser's own read blocking. Even an ordinary GET that sends a disallowed Origin can execute on the server. Sensitive actions must be protected by separate authentication, authorization, and CSRF policies.

What you will do in the next lab

Eight steps lead to one runnable result. Validate the origin format → remove duplicate origins → limit the methods to an allow list → do not allow credentials together with an asterisk → attach the real CORS middleware → build the preflight request → compute the rejection matrix → observe the difference between CORS and authentication.

Each step checks actual return values, exceptions, and state changes, not the fact that a function or file exists. After you see the answer, deliberately change a boundary comparison or the cleanup code and check which tests fail. Explain why the earlier tests are kept in the next steps, and write down one operating condition that this lab does not guarantee.