9 Authentication, Authorization & CORS
Learning Objectives
By the end of this lecture you should be able to:
Distinguish authentication (who are you?) from authorization (what are you allowed to do?) from CORS (is this browser origin allowed to see the response?), and explain why conflating any two of them causes real security bugs.
Describe the three parts of a JWT (header, payload, signature), state precisely what the signature does and does not guarantee, and explain why a JWT payload must never hold secrets.
Compare session-based and token-based authentication along concrete axes (server state, revocation, scaling) without treating either as unconditionally “better.”
Weigh the tradeoffs of storing a token in localStorage versus an HttpOnly cookie, and articulate what each does and does not protect against.
Implement a reusable FastAPI authentication dependency that extracts, verifies, and decodes a Bearer token, and layer role-based authorization on top of it with a dependency factory.
Correctly choose between a 401 and a 403 response for a given failure, and explain the semantic difference to another developer.
Explain the same-origin policy, configure FastAPI’s CORSMiddleware correctly, and predict when a browser will send a preflight OPTIONS request.
Debug a CORS failure systematically using browser DevTools instead of guessing, and recognize (and avoid) the four most common CORS misconfigurations.
Trace one HTTP request end to end through CORS, authentication, and authorization, and say which layer would reject it and why.
Motivation: TerpTasks Needs to Know Who’s Asking
TerpTasks, by the end of the REST/FastAPI and React lectures, is a
working full-stack app: a FastAPI backend with clean CRUD endpoints,
a React frontend that renders and edits tasks, CORS enabled so the
two can even talk to each other. There is exactly one problem, and
it’s a big one: every endpoint trusts every request. Anyone
who can reach the server —
TerpTasks is about to become a real course-management tool, which means it needs users with different jobs:
a student should see and manage only their own tasks,
an instructor should be able to create assignments for a course, and
an admin should be able to manage the whole system.
Getting from “every request is trusted” to “the server knows who you are and what your role allows” takes three distinct pieces, introduced in the order the plan above walks through them:
User |
| |
Login |
| |
FastAPI authenticates credentials |
| |
JWT issued |
| |
React stores/sends token |
| |
Authorization: Bearer <token> |
| |
FastAPI verifies JWT |
| |
Check user role |
| |
Allow / reject request |
Every section below fills in one link of that chain. By the end,
TerpTasks will run a login endpoint, issue signed tokens, verify them
on every protected route, and enforce role-based rules —
9.1 Authentication vs. Authorization
Formal definition. Authentication answers who are
you? —
Plain English. Authentication is showing your CampusID at the
building door; authorization is whether that card actually opens
this door. The card reader doesn’t re-check your identity for
every door in the building —
username + password |
| |
Authentication |
| |
"Alice" |
Alice |
| |
role = instructor |
| |
Can create assignments? |
Yes |
Intuition —
A third concept belongs in the same table, and this lecture spends its second half on it:
Concept | Question | TerpTasks example |
Authentication | Who are you? | Alice logs in |
Authorization | What can you do? | Alice (instructor) can create assignments |
CORS | Can this browser frontend call this backend? | React (:5173) -> FastAPI (:8000) |
Misconception. “If CORS is configured, the API is protected.” Hold onto this now, because it’s worth repeating after every section: CORS is not an authentication mechanism, and it is not an authorization mechanism. It is a browser-only rule about which origins may read a response. It says nothing about who the user is or what they’re allowed to do, and it does nothing at all against a client that isn’t a browser. Section CORS makes this precise; for now, just don’t let the three rows of that table blur into one.
9.2 JWT Structure
TerpTasks needs a way to represent “the server already checked this person’s password, and here’s who they are” so that later requests don’t have to send the password again. A JWT (JSON Web Token) is the standard shape for that claim.
Formal definition. A JWT is a compact, URL-safe string encoding a set of claims (statements about a subject), cryptographically signed so that any tampering is detectable. It has three dot-separated parts:
xxxxx.yyyyy.zzzzz |
| | | |
header payload signature |
9.2.1 Header
The header conceptually looks like this:
{
"alg": "HS256",
"typ": "JWT"
}alg names the signing algorithm; typ just says “this is a
JWT.” Nothing here is secret —
9.2.2 Payload
The payload is where TerpTasks puts the actual claims about the authenticated user:
{
"sub": "alice",
"role": "instructor",
"exp": 1780000000
}sub (subject) is the user identifier; exp is an expiration
timestamp; iat (issued-at) is common too. role is not part
of the JWT standard —
9.2.3 Signature
signature = |
Sign( |
base64url(header) + "." + base64url(payload), |
secret |
) |
The signature is computed over the header and payload using a secret (or private key) only the server knows, and it’s what makes the token trustworthy: change one character of the payload, and re-verifying the signature fails.
The critical point, worth its own paragraph: JWT
payloads are encoded, not encrypted. Base64url encoding is not a
cipher —
Mini demonstration. Paste a real JWT into jwt.io (or decode it by hand: base64url-decode the first two dot-separated sections) and ask:
Which part can the browser decode? The header and the payload —
both are plain base64url, no key required. Can the browser change "role": "student" to "role": "admin" and produce a token the server still accepts? No —
assuming the backend actually re-verifies the signature on every request (Section The Authorization Header and FastAPI Authentication builds exactly that check). Editing the payload changes its bytes, so the old signature no longer matches; without the server’s secret, there’s no way to compute a new signature that does.
The entire security property of a JWT rests on that one fact: the payload is public, but only the server can produce a signature the server itself will accept.
9.3 Sessions vs. Token-Based Authentication
JWTs are one way to represent “this request comes from an authenticated user.” The traditional alternative is a session, and TerpTasks could have been built either way.
9.3.1 Session-based
Browser |
| |
| session cookie |
v |
Server |
| |
v |
Session store |
The browser holds a small, opaque session id in a cookie; the server looks that id up in a session store (in memory, in Redis, in a database table) to find out who’s asking and what they’re allowed to do. The server maintains the state.
9.3.2 Token-based
Browser |
| |
| JWT |
v |
Server |
| |
v |
Verify token |
The browser holds a JWT; the server verifies its signature and reads
the claims directly out of the token. No lookup is required —
9.3.3 Comparison
Sessions | JWT / token | |
Server state | usually required (a session store) | can be stateless |
Revocation | relatively straightforward (delete the row) | more complicated (see the appendix) |
Scaling | needs shared session storage or sticky sessions | easier to distribute across servers |
Token size | small cookie/session id | can be larger (claims travel with it) |
Storage strategy | cookie-based, mostly settled | matters --- see the next section |
Common use | traditional server-rendered web apps | APIs, mobile apps, single-page apps |
Don’t present JWT as automatically “better.” It trades a
server-side lookup for a cryptographic check, which is a genuine win
for horizontally scaled APIs with no shared session store —
JWT solves token representation and verification; it does not
automatically solve every authentication problem. It’s the right
tool when statelessness and scaling matter more than instant
revocation; a session is often still the right tool for a
traditional server-rendered site. TerpTasks, as a React SPA talking
to a FastAPI API, is exactly the profile that motivates the token
approach —
9.4 Storing Tokens and Sending Them
Once FastAPI hands React a JWT, React has to keep it somewhere and attach it to every future request. This is the section that should generate the most discussion, because both common answers have real security tradeoffs and neither is simply “the secure one.”
9.4.1 Option 1: localStorage
localStorage.setItem("token", jwt);and then on every request:
Authorization: Bearer eyJ... |
Advantages. Trivial to read and write from JavaScript; convenient for an SPA that attaches the token itself; the frontend code has full, explicit control over when the token is sent.
Disadvantage. Anything that can run JavaScript on your page
can also run localStorage.getItem("token"). If an attacker
achieves XSS (cross-site scripting —
9.4.2 Option 2: HttpOnly cookie
The server sets the cookie instead of handing the token to JavaScript at all:
Set-Cookie: session=...; HttpOnly; Secure |
HttpOnly tells the browser: never expose this cookie’s
value to JavaScript, full stop —
Advantage. Because JavaScript can’t read it, even a successful XSS injection can’t directly steal the token by calling localStorage.getItem or document.cookie.
Tradeoff. Cookies are attached to requests automatically
by the browser, for any request to the cookie’s domain —
The important teaching point. Resist the shortcut:
“Cookies are secure and localStorage is insecure.”
is a false sentence people repeat anyway. The accurate one:
both approaches have different security properties and need to
be configured correctly. localStorage is vulnerable to XSS
reading the token directly; HttpOnly cookies are not, but open
up CSRF if you don’t add the matching protections. Neither choice is
a substitute for not having XSS-vulnerable code in the first place —
9.5 The Authorization Header and FastAPI Authentication
Whichever storage strategy React uses, the token has to travel to FastAPI on every protected request. The standard vehicle is the Authorization header with the Bearer scheme:
GET /api/courses |
|
Authorization: Bearer eyJhbGciOi... |
Authorization |
| |
Bearer |
| |
JWT |
Bearer means exactly what it sounds like: whoever holds
this token is treated as authenticated —
9.5.1 The authentication dependency
FastAPI’s answer to “check this on every protected route” is dependency injection: write the check once, as a function, and let every route that needs it declare Depends(...) on it. OAuth2PasswordBearer is a small helper that knows how to pull the token out of the Authorization: Bearer ... header (and tells FastAPI’s auto-generated docs how to prompt for it):
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
app = FastAPI()
SECRET_KEY = "change-me-in-production" # load from an env var, never hard-code
ALGORITHM = "HS256"
# tokenUrl tells the interactive /docs page where to POST for a token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise credentials_error
except jwt.InvalidTokenError:
raise credentials_error
username = payload.get("sub")
role = payload.get("role")
if username is None or role is None:
raise credentials_error
return {"username": username, "role": role}Trace what this dependency does, in order, matching the steps from the motivation diagram:
Extracts the token —
oauth2_scheme pulls the raw string out of the Authorization header (a missing header raises 401 before this function even runs). Verifies the signature —
jwt.decode recomputes the signature with SECRET_KEY and rejects the token if it doesn’t match. Checks expiration —
jwt.decode also rejects an expired exp claim automatically, raising ExpiredSignatureError. Extracts the user identity —
reads sub and role out of the now-trusted payload. Returns the authenticated user for the route to use.
And the endpoint that uses it:
@app.get("/api/courses")
async def get_courses(user: dict = Depends(get_current_user)):
return {"courses": [...], "requested_by": user["username"]}The key FastAPI concept: authentication becomes a
reusable dependency. Every protected route adds one parameter —
9.5.2 Issuing the token: the login endpoint
get_current_user verifies tokens; something has to issue them first, after checking a password:
from datetime import datetime, timedelta, timezone
from fastapi.security import OAuth2PasswordRequestForm
def create_access_token(sub: str, role: str, expires_minutes: int = 30) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": sub,
"role": role,
"iat": now,
"exp": now + timedelta(minutes=expires_minutes),
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
@app.post("/auth/login")
async def login(form: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form.username, form.password) # checks a hashed password
if user is None:
raise HTTPException(status_code=401, detail="Incorrect username or password")
token = create_access_token(sub=user.username, role=user.role)
return {"access_token": token, "token_type": "bearer"}authenticate_user is ordinary application code —
9.6 RBAC: Role-Based Access Control
Authentication (the dependency above) answers who. TerpTasks
still needs to answer what are they allowed to do —
Authentication |
| |
Who is Alice? |
| |
Alice |
| |
What is Alice's role? |
| |
instructor |
| |
Is instructor allowed? |
| |
Yes / No |
Formal definition. RBAC (role-based access control) grants permissions to roles (student, instructor, admin) rather than to individual users, and assigns each user one or more roles. Checking authorization becomes “does this user’s role have this permission,” not “does this specific person.”
9.6.1 Dependency-based authorization
The same dependency-injection idea from Section
The Authorization Header and FastAPI Authentication composes naturally: write a
dependency factory —
def require_role(required_role: str):
async def checker(user: dict = Depends(get_current_user)) -> dict:
if user["role"] != required_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden",
)
return user
return checkerrequire_role("instructor") builds on top of get_current_user: it authenticates first, then checks the role, so a route that uses it gets both checks, in the correct order, from one Depends:
@app.post("/api/assignments")
async def create_assignment(user: dict = Depends(require_role("instructor"))):
...401 vs. 403
This distinction is worth its own callout, because the two are routinely confused and the confusion leaks into API responses that mislead every client that has to handle them.
401 Unauthorized —
No token |
Invalid token |
Expired token |
Every one of these is an authentication failure: the server never established an identity, so it can’t even ask the authorization question yet.
403 Forbidden —
Authenticated user |
| |
role = student |
| |
POST /api/assignments |
| |
403 Forbidden |
The student authenticated successfully —
Mnemonic: 401 = “who are you?” 403 = “I know
who you are, but no.” Returning 403 for a missing token (there
was no “who” to know) or 401 for a wrong role (the server
does know who this is) is a common and avoidable API-design
mistake —
9.7 CORS
Every piece so far assumed the request already reached FastAPI. Before it can, when the caller is a browser, one more gate has to open: CORS.
React |
http://localhost:5173 |
|
| HTTP request |
|
FastAPI |
http://localhost:8000 |
Why does the browser care at all? Without any restriction,
JavaScript running on any page you happen to visit could
silently make requests to any other site using your browser —
Formal definition. An origin is the triple
scheme + host + port. http://localhost:3000 and
http://localhost:8000 are different origins —
9.7.1 What CORS actually does
CORS (Cross-Origin Resource Sharing) is the mechanism by which a server tells browsers which other origins may read its responses. In FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)This says, roughly: allow the frontend at
http://localhost:3000 to make cross-origin requests to this
FastAPI app, and let the browser expose the response to that page’s
JavaScript. Without it, the request may still reach the
server (the server processes it and sends a response) —
CORS is primarily a browser enforcement mechanism. This is the single most important sentence in the section, because of what follows from it:
CORS does not stop someone from using curl, Postman, or another backend from calling your API. Those clients aren’t browsers; they don’t enforce the same-origin policy at all, and a bare HTTP request has no concept of “origin” to check in the first place.
Therefore, restated from Section Authentication vs. Authorization with the full mechanism now in view:
CORS != authentication |
CORS != authorization |
An allow_origins list that includes http://localhost:3000
says nothing about whether a request from that origin is logged in
or permitted to do anything —
9.7.2 The Authorization header and CORS
This is the seam where all three concepts of this lecture touch one request at once. React sends:
GET /api/users |
Authorization: Bearer eyJhbGciOi... |
A custom header like Authorization is exactly the kind of thing CORS has to be told to allow through:
allow_headers=["Authorization", "Content-Type"]React |
| |
| Authorization: Bearer JWT |
v |
FastAPI |
| |
+-- CORS check |
| |
+-- JWT verification |
| |
+-- Authorization/RBAC check |
Three different gates, three different failure modes, three different fixes: a CORS failure is fixed in CORSMiddleware’s configuration, a JWT failure is fixed in get_current_user, and an RBAC failure is fixed in require_role. Knowing which gate rejected a request is the entire debugging skill of this lecture’s second half.
9.7.3 Preflight requests
Some cross-origin requests aren’t sent directly —
OPTIONS /api/users |
Origin: http://localhost:3000 |
Access-Control-Request-Method: GET |
Access-Control-Request-Headers: Authorization |
The server answers:
Access-Control-Allow-Origin: http://localhost:3000 |
Access-Control-Allow-Methods: GET, POST |
Access-Control-Allow-Headers: Authorization, Content-Type |
and only if the browser is satisfied with that answer does it send the actual request:
GET /api/users |
Authorization: Bearer ... |
Why bother? Because some cross-origin requests can have real side effects on the server, the browser wants the server to explicitly say “yes, this method, these headers, from this origin, are fine” before it commits to sending the real one. A custom header such as Authorization is one of the common triggers for this preflight behavior:
Cross-origin request |
| |
+-- "simple" request |
| | |
| actual request |
| |
+-- non-simple request |
| |
OPTIONS |
(preflight) |
| |
actual request |
You don’t need the formal specification’s exact boundary between
“simple” and “non-simple” for this course —
9.8 Common CORS Mistakes and Debugging
Four mistakes account for almost every CORS bug you’ll hit this semester.
1. Wrong origin. allow_origins=["http://localhost:3000"]
does not match a frontend actually running at
http://localhost:5173 —
2. Missing protocol. "localhost:3000" is not a valid origin string; it must be "http://localhost:3000", scheme included.
3. Credentials + wildcard. Don’t casually combine
allow_credentials=True
allow_origins=["*"]Credentialed cross-origin requests (cookies, or an Authorization header sent with credentials mode) come with tighter browser restrictions specifically because a wildcard origin plus credentials would let any site ride along with a logged-in user’s session. List explicit origins once credentials are in play.
4. Forgetting headers. The frontend sends
Authorization: Bearer ..., but allow_headers doesn’t permit
it —
9.8.1 The debugging workflow
Don’t guess. Open:
Browser DevTools |
| |
Network |
| |
Request |
| |
Request Headers |
Response Headers |
| |
Look for Access-Control-* headers |
and check, in order: the request URL, the Origin header actually sent, whether an OPTIONS preflight happened at all, its response status, and whether Access-Control-Allow-Origin / -Allow-Methods / -Allow-Headers match what the real request needs.
OPTIONS /api/users |
| |
403 |
| |
CORS configuration problem |
versus the working case:
OPTIONS /api/users |
| |
200 |
| |
GET /api/users |
| |
200 |
The most important lesson of this section: read the
browser’s CORS error message and inspect the actual request/response
in the Network tab —
9.9 Putting It All Together: The Full Security Flow
Every piece from this lecture is one link in a single chain. Trace a request for /api/courses end to end:
+-----------------+ |
| React | |
+--------+--------+ |
| |
Login request |
| |
v |
+-----------------+ |
| FastAPI | |
+--------+--------+ |
| |
Verify credentials |
| |
v |
Issue JWT |
| |
v |
+-----------------+ |
| React | |
+--------+--------+ |
| |
Authorization: Bearer JWT |
| |
v |
+-----------------+ |
| FastAPI | |
| | |
| CORS check | |
| | | |
| Verify JWT | |
| v | |
| Identify user | |
| v | |
| Check role | |
+--------+--------+ |
| |
+------+------+ |
| | |
allowed denied |
| | |
v v |
200 401/403 |
9.9.1 Five takeaways
Authentication establishes who the user is.
Authorization determines what the authenticated user can do.
JWTs contain signed claims; they are not encrypted by default —
never put secrets in the payload. RBAC composes with FastAPI dependencies, and should distinguish 401 (no identity yet) from 403 (identity known, action denied).
CORS is a browser security mechanism, not an authentication or authorization mechanism —
it decides who may read a response, not who may make a request.
Appendix: Refresh Tokens and Revocation
Not covered in the 75-minute lecture, but the direct answer to
the “revocation is more complicated” row from Section
Sessions vs. Token-Based Authentication’s comparison table —
A JWT that’s valid for 30 minutes is a problem the moment you need to
kill it sooner —
Two patterns fix this in practice:
Short-lived access tokens plus a refresh token. Issue a JWT that expires in minutes (an access token) alongside a second, longer-lived refresh token stored server-side (so it can be revoked, e.g. by deleting its database row). The client trades the refresh token for a new access token every few minutes; revoking access now means invalidating the refresh token, and the damage window from a stolen access token shrinks to its short expiry instead of the full session length.
@app.post("/auth/refresh")
async def refresh(refresh_token: str):
stored = get_refresh_token(refresh_token) # a DB lookup -- revocable
if stored is None or stored.revoked:
raise HTTPException(status_code=401, detail="Invalid refresh token")
return {"access_token": create_access_token(stored.username, stored.role)}A denylist for genuinely urgent revocation. Keep a small,
fast store (e.g. Redis) of token identifiers (a jti claim) that
must be rejected immediately even though they haven’t expired —
Neither pattern is free: both trade back some of the statelessness that made JWTs attractive in Section Sessions vs. Token-Based Authentication in exchange for the revocation control a plain JWT can’t offer on its own. That tradeoff, not a a flaw in JWTs themselves, is the honest answer to “how do I log someone out.”
9.10 GitHub-Ready Mini-Project: terptasks-auth
See the project on GitHub
Practice Exercises
Basic
1. Decode a JWT by hand. Take any JWT (e.g. from jwt.io’s example), split it on ., base64url-decode the first two parts, and show the resulting header and payload JSON. Confirm you did not need the secret to do this.
2. 401 or 403? For each of the following, say which status code TerpTasks should return and why: (a) no Authorization header at all, (b) a well-formed but expired token, (c) a valid token belonging to a student hitting POST /api/assignments, (d) a token signed with the wrong secret.
3. Spot the CORS mistake. Given allow_origins=["localhost:3000"] and a frontend running at http://localhost:3000, explain exactly why the browser still blocks the request, and fix the configuration.
Intermediate
4. Add an admin role. Extend require_role to accept a list of acceptable roles (e.g. require_role(["instructor", "admin"])) instead of exactly one, and add an admin-only DELETE /api/courses/{id} route.
5. Trigger and read a real preflight. Run the mini-project with the React dev server, open DevTools’ Network tab, and capture an OPTIONS request. Identify the Access-Control-Request-Method and Access-Control-Request-Headers the browser sent, and match them against the server’s response headers.
6. Break the signature on purpose. Take a valid token from the mini-project, flip one character in the payload segment (re-encode it), and send it to /api/courses. Confirm you get 401, and explain in one sentence why changing the payload alone is enough to invalidate it.
Advanced
7. Implement the refresh-token appendix. Add a server-side refresh token table (even an in-memory dict keyed by a random id is fine), a POST /auth/refresh route, and a POST /auth/logout route that revokes a specific refresh token. Write a test that logs in, refreshes, then confirms a revoked refresh token can no longer mint new access tokens.
8. Move the token to an HttpOnly cookie. Change /auth/login to set the JWT via Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax instead of returning it in the response body, and update get_current_user to read it from the cookie instead of the Authorization header. Explain what new protection (CSRF) this reintroduces and sketch one mitigation.
9. Complete exercise 12 from the React lecture. Add an Axios interceptor (or fetch wrapper) in the TerpTasks frontend that attaches the stored token to every request and, on a 401 response, clears it and redirects to a login screen —
without touching any of the existing API call sites. Then demonstrate that a 403 does not trigger the same redirect, and explain why that distinction matters for the user experience.
Summary
Key takeaways
Authentication, authorization, and CORS are three different questions. Who are you, what can you do, and can this browser origin see the response —
keep them in separate boxes, and know which one a given failure belongs to. A JWT’s payload is public; only its signature is trusted. Base64url is an encoding, not encryption —
never put secrets in the payload, and treat signature verification as the only thing standing between a legitimate token and a forged one. Tokens are not automatically better than sessions. They trade a server-side lookup for statelessness and easier scaling, at the cost of harder revocation —
pick deliberately, and see the appendix if instant revocation matters. Where you store a token has real security consequences. localStorage is readable by any script that runs on your page (XSS risk); an HttpOnly cookie is not, but is sent automatically by the browser (CSRF risk). Neither is a free win.
Authentication is a reusable FastAPI dependency; RBAC composes on top of it. Depends(get_current_user) for identity, Depends(require_role(...)) for permission —
write each check once. 401 means “I don’t know you”; 403 means “I know you, and no.” Getting this right keeps your API’s error responses meaningful to every client that has to branch on them.
CORS is enforced by browsers, not by your API. A curl request or another backend ignores it entirely —
CORS configuration is not a substitute for authentication or authorization, ever.
Terminology
Term | Definition |
Authentication | Establishing who a user is |
Authorization | Deciding what an authenticated user may do |
JWT | A signed, base64url-encoded token carrying claims (header.payload.signature) |
Claim | A statement in a JWT payload, e.g. sub, exp, role |
Bearer token | A token that authenticates whoever holds it, no further proof required |
Session | Server-side state referenced by an opaque cookie value |
RBAC | Role-based access control: permissions attached to roles, not individual users |
401 Unauthorized | No valid authenticated identity was established |
403 Forbidden | Identity is known; the action is not permitted |
Same-origin policy | Browser rule blocking cross-origin script access to responses by default |
Origin | scheme + host + port; any difference makes two URLs different origins |
CORS | Server-granted, browser-enforced permission for a specific cross-origin request |
Preflight | An OPTIONS request the browser sends to ask permission before a non-simple request |
XSS | Attacker script executing in your page, e.g. via unsanitized rendered input |
CSRF | A malicious page triggering a request that rides on the victim’s existing cookies |
Common mistakes
Treating a valid CORS configuration as proof the API is secure —
it only controls what browsers may read, nothing about who is allowed to ask. Putting a password, API key, or other secret inside a JWT payload because “it’s encoded.”
Returning 403 for a missing/invalid token, or 401 for a valid token with the wrong role —
swapping the two confuses every client that branches on status code. Storing a JWT in localStorage and calling it “done” with no thought to XSS, or switching to an HttpOnly cookie and forgetting CSRF protections entirely.
Combining allow_credentials=True with allow_origins=["*"].
Guessing at a CORS fix instead of reading the actual Origin/Access-Control-* headers in DevTools’ Network tab.
Forgetting that a missing FK-style check exists here too: an endpoint that authenticates a user but never checks whose task is being edited (Alice can edit Bob’s task by guessing its id) has an authorization hole even with RBAC in place —
role checks and ownership checks are not the same thing.
Connections
Backward: the REST/FastAPI lecture’s dependency-injection pattern (Depends) is exactly what makes get_current_user/require_role reusable across routes; its status-code discipline is what makes 401 vs.\ 403 matter; the React lecture’s Appendix B introduced CORS as “the full-stack gotcha,” and this lecture is where it gets a full mechanism (same-origin policy, preflight) and a permanent home next to authentication and authorization.
Forward: the database lecture’s users table is where authenticate_user really looks up hashed passwords instead of an in-memory dict; the layered-architecture lecture’s service/route split is exactly where get_current_user and require_role live as cross-cutting concerns; and any later deployment work will move SECRET_KEY out of source code and into environment configuration, and allow_origins from localhost to a real production domain.