MAD Study Hub

Home / MAD2 / Week 8

Week 8 · Token authentication, API design, CORS and cookies

Official syllabus line: Authentication and Designing APIs
About 95 minQuiz 2 material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 week 7 (Vuex store, token in storage), MAD1 Flask and Flask-RESTful (the API weeks), HTTP status codes.

The one-minute story

Think of it like this

Token authentication is a cinema wristband. At the ticket counter (the login endpoint) you show your ID and pay once. They give you a wristband (the token) with a stamp that only the cinema can make (the signature) and an expiry time printed on it. After that, every guard at every door just looks at the wristband: no need to go back to the counter, no register of who is inside. If the band is torn, fake or old, the guard says no.

Session authentication is the older way: the counter writes your name in a register (the session on the server) and gives you only a number. Each door guard must phone the register to check. Fine for one building; awkward for ten buildings.

Then there are rules of the street: CORS (which other websites may use this cinema's doors), cookies (wristbands the browser attaches by itself), and API design (how to name doors and what signs to show when someone is refused).

By the end of this week you can:

  • explain the difference between session and token authentication and trace a token flow from login to expiry;
  • read a JWT, say what is in each part, and explain why the payload is readable but cannot be forged;
  • protect Flask routes with Flask-Security-Too or Flask-JWT-Extended, and check roles on the server and in Vue Router;
  • choose where to store a token and name the XSS and CSRF risks;
  • explain CORS, simple versus preflighted requests, and cookie attributes SameSite, HttpOnly, Secure;
  • design a clean REST API: names, verbs, status codes, error JSON, pagination, versioning.

Words you need

Words you need
  • Authentication - proving who you are (login). Authorization - what you are allowed to do (roles).
  • Session - login state kept on the server, found with an id that usually travels in a cookie.
  • Token - a string the client gets at login and sends with every request instead of a password.
  • JWT (JSON Web Token) - a token made of three base64url parts, header.payload.signature, that carries claims and is signed.
  • Claim - one field inside a JWT payload (sub, exp, role).
  • Bearer token - "whoever bears (carries) this token gets in"; sent as Authorization: Bearer <token>.
  • Refresh token - a long-lived token used only to get a new short-lived access token.
  • RBAC - role-based access control: permissions come from the user's role.
  • Origin - scheme + host + port, for example http://localhost:8080.
  • CORS - the browser rule (relaxed by server headers) for letting JavaScript from one origin read responses from another.
  • Preflight - the automatic OPTIONS request a browser sends before a "non-simple" cross-origin request.
  • XSS - cross-site scripting: attacker's script runs in your page. CSRF - cross-site request forgery: another site makes your browser send an authenticated request.
  • Idempotent - repeating the request has the same effect as doing it once (GET, PUT, DELETE).
  • Pagination - returning a big list in pages.

Idea 1: Session versus token authentication

Session (cookie)Token (header)
Where login state livesServer memory or databaseIn the token itself (JWT) or a server lookup (opaque token)
What the client keepsA session id in a cookieThe token in memory, localStorage or a cookie
How it is sentBrowser attaches the cookie automaticallyYour JavaScript adds an Authorization or Authentication-Token header
Scaling to many serversNeeds shared session storageAny server can verify the signature, no shared store
Works for mobile apps and other backendsAwkwardNatural
Main extra riskCSRF (the cookie goes automatically)XSS if the token sits in localStorage

A real question: a Vue frontend, a mobile app and another service call the same API on several servers. JWT fits, because each request carries the token and any server can validate it without storing a per-user session in memory. Saying that sessions are easier to scale because "no shared storage is needed" is false.

The idea

Token flow, end to end.

  1. Client sends credentials: POST /login with JSON {username, password}.
  2. Server checks the password (against a hash), creates a token and returns it, with an expiry.
  3. Client stores the token (say, in the Vuex store, backed by browser storage).
  4. Every later request carries the token: Authorization: Bearer eyJ... (Flask-Security: Authentication-Token: ...).
  5. Server validates signature and expiry, finds the user, runs the route, returns data. Missing or bad token: 401. Valid token but not allowed: 403.
  6. When the token expires, the user logs in again (or a refresh token fetches a new one).

A false statement in the papers: "The client must send the token with the first request and need not send it in the subsequent requests". Wrong: it must be sent with every protected request, because the server keeps no memory of you. Another: "with Flask-Security all endpoints are protected by default". Wrong: you protect each route with a decorator such as @auth_required('token').

Idea 2: JWT structure, and why you can read it but cannot forge it

A JWT is three base64url strings joined by dots: header.payload.signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc5MTI3MTM0NH0.qEbqtAXKX3D5uxeiTLxLNjdK8Ychmy2str4jB1JBUAY
PartContainsDecoded (this example)
Headeralgorithm and type{"alg":"HS256","typ":"JWT"}
Payloadthe claims{"sub":"42","role":"admin","exp":1791271344}
SignatureHMAC of header.payload with a secret keybinary, not JSON

Each of the first two parts is JSON encoded in base64url (base64 with - and _ and no = padding). Base64 is an encoding, not encryption: anyone can decode it. So the payload is readable by anyone who holds the token. Never put passwords or secrets in it.

The signature is what makes it trustworthy: the server computes HMAC_SHA256(secret, header + "." + payload). If someone edits the payload (say role to superadmin), the signature no longer matches and the server rejects the token, because the attacker does not know the secret. So: readable but tamper-evident.

I ran this with PyJWT:

import jwt, time
SECRET = "use-a-long-random-secret-of-at-least-32-bytes"
token = jwt.encode({"sub": "42", "role": "admin", "exp": int(time.time()) + 3600},
                   SECRET, algorithm="HS256")
print(jwt.decode(token, SECRET, algorithms=["HS256"]))
# {'sub': '42', 'role': 'admin', 'exp': ...}

jwt.decode(token, "wrong-key", algorithms=["HS256"])
# jwt.exceptions.InvalidSignatureError: Signature verification failed

An expired token raises ExpiredSignatureError, and a token whose payload was edited raises InvalidSignatureError. In recent PyJWT the sub claim must be a string, which is why the identity above is "42". Always pass algorithms=[...] to decode.

The idea

Standard claims: sub (subject, the user id), exp (expiry time), iat (issued at), iss (issuer). HS256 uses one shared secret key to sign and verify. RS256 uses a private key to sign and a public key to verify. A JWT is stateless: the server needs no lookup, but this also means you cannot easily revoke one before it expires. That is why access tokens are made short-lived.

Real statements: "JWT has three components, header, payload and signature" (true); "each component is base64 encoded" (true); "components are joined by a period" (true); "it contains XML" or "only the payload is contained" (false).

Idea 3: Token auth in Flask

Flask-Security-Too (the course's usual choice)

from flask_security import Security, SQLAlchemyUserDatastore, auth_required, roles_required

app.config["SECRET_KEY"] = "dev-secret"
app.config["SECURITY_PASSWORD_SALT"] = "dev-salt"
app.config["SECURITY_TOKEN_AUTHENTICATION_HEADER"] = "Authentication-Token"   # the default
app.config["WTF_CSRF_ENABLED"] = False        # typical for a pure JSON API

datastore = SQLAlchemyUserDatastore(db, User, Role)
app.security = Security(app, datastore)

@app.get("/api/profile")
@auth_required("token")                         # only token auth accepted here
def profile():
    return {"email": current_user.email}

@app.get("/api/admin")
@auth_required("token")
@roles_required("admin")                        # 403 if the user lacks the role
def admin():
    return {"ok": True}

Login: POST /login?include_auth_token with JSON credentials. The response JSON holds the token (in response.user.authentication_token); the client sends it back in the header Authentication-Token. Passwords are stored hashed (Flask-Security hashes them; with plain Flask use generate_password_hash and check_password_hash). Take the exact config names and the exact response shape from your course notes and the version you installed.

Flask-JWT-Extended outline (tested)

from flask_jwt_extended import (JWTManager, create_access_token, jwt_required,
                                get_jwt_identity, get_jwt)
app.config["JWT_SECRET_KEY"] = "a-long-random-secret"
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=15)
JWTManager(app)

@app.post("/api/v1/login")
def login():
    d = request.get_json(silent=True) or {}
    u = USERS.get(d.get("username"))
    if not u or not check_password_hash(u["hash"], d.get("password", "")):
        return jsonify(error="bad credentials"), 401
    token = create_access_token(identity=str(u["id"]), additional_claims={"role": u["role"]})
    return jsonify(access_token=token)

@app.get("/api/v1/me")
@jwt_required()
def me():
    return jsonify(id=get_jwt_identity(), role=get_jwt()["role"])

I ran this: with no header, /me gives 401 {'msg': 'Missing Authorization Header'}; with Authorization: Bearer <token> it returns {'id': '1', 'role': 'admin'}. get_jwt_identity() reads the identity stored in the token.

Idea 4: Roles, and why the server check is the real security

RBAC: each user has a role (admin, user, sponsor...). The role decides which routes and actions are allowed.

def role_required(role):
    def deco(fn):
        @wraps(fn)
        @jwt_required()
        def wrapper(*args, **kwargs):
            if get_jwt().get("role") != role:
                return jsonify(error="forbidden"), 403
            return fn(*args, **kwargs)
        return wrapper
    return deco

@app.get("/api/v1/admin")
@role_required("admin")
def admin_area(): ...

The Vue side (Vue Router navigation guard) only improves the user experience:

router.beforeEach((to, from, next) => {
  const token = store.state.token
  const role = store.state.role
  if (to.meta.requiresAuth && !token) next('/login')
  else if (to.meta.role && to.meta.role !== role) next('/forbidden')
  else next()
})
// routes: { path: '/admin', component: Admin, meta: { requiresAuth: true, role: 'admin' } }
The idea

The browser is not trusted. A user can open devtools, edit the Vuex state, or call your API with curl, skipping the Vue router completely. A hidden button or a router guard stops honest users from wandering; only the server check (decorator, 401/403) protects data. Always do both, but never rely on the front end alone.

Solved example: 1. Which status code?

Match each situation to a code.

  • Request with no token to a protected route: 401 Unauthorized (you are not identified).
  • Valid token of a normal user calling an admin-only route: 403 Forbidden (identified, not allowed).
  • Valid token, expired: 401 (the client should log in again or refresh).
  • Wrong password at login: 401.
  • Token is fine, resource id does not exist: 404.

Idea 5: Where to keep the token, XSS and CSRF

PlaceSurvives refreshJavaScript can read itSent automaticallyMain risk
JS memory (Vuex, a variable)noyesnolost on refresh; XSS can still use it while the page runs
sessionStorageyes (same tab)yesnoXSS can steal it
localStorageyes (also after restart)yesnoXSS can steal it; lives long
Cookie, plainyesyesyesCSRF, and XSS can read it
Cookie, HttpOnlyyesnoyesCSRF (needs SameSite and a CSRF token), safe from script theft
  • XSS: the attacker gets their script to run on your site (for example through an unescaped comment). That script can read localStorage and send your token away. Defence: escape output (Vue's {{ }} escapes by default), avoid v-html on user input, keep tokens short-lived.
  • CSRF: you are logged in to a bank in one tab; a malicious page triggers a request such as an image whose src is http://example1.com/delete?id=1. The browser attaches your cookie automatically and the server thinks it is you. This real exam scenario is CSRF, not XSS. It works because cookies are sent automatically. A token placed in a header by your own JavaScript is not sent by a foreign page. Defences: CSRF tokens, SameSite cookies, requiring a custom header, never changing data with GET.
  • Flask does not enforce CSRF protection by default, and does not send CORS headers by default.

Refresh token idea: the login returns two tokens. The access token lives 5 to 15 minutes and goes on every request. The refresh token lives days, is sent only to a /refresh endpoint and returns a new access token. If the access token is stolen the damage window is short, and the user does not have to type the password every 15 minutes.

Idea 6: CORS in depth

Two URLs have the same origin only if scheme, host and port all match. http://localhost:8080 (Vue dev server) and http://localhost:5000 (Flask) are different origins because the port differs.

Browsers enforce the same-origin policy: JavaScript from origin A may not read a response from origin B unless B says so. CORS is how B says so, with response headers. Notes the papers test:

  • CORS lets a server declare which origins may load its resources. It does not encrypt, compress or authenticate.
  • Same-origin requests need no CORS.
  • It is the browser that blocks. curl and Postman ignore CORS. The request often still reaches the server; it is the response the page may not read.
  • By default Flask sends no CORS headers, so a fetch from another origin fails until you enable them.
from flask_cors import CORS
CORS(app, origins=["http://localhost:8080"])      # allow just the Vue dev server

Simple versus preflighted. A simple request is a GET, HEAD or POST with a basic content type (text/plain, form data) and no custom headers. Sending JSON (Content-Type: application/json), an Authorization header, or using PUT or DELETE makes it non-simple. Then the browser first sends an automatic preflight:

OPTIONS /api/v1/me
Origin: http://localhost:8080
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization

The server answers (I captured this from Flask-CORS):

Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
Access-Control-Allow-Headers: authorization

If these match, the browser sends the real request, and the real response also carries Access-Control-Allow-Origin. For an origin that is not on the list the header is simply missing, and the browser refuses to give the response to the page.

HeaderMeaning
Access-Control-Allow-Originwhich origin may read the response (* or one origin)
Access-Control-Allow-Methodsmethods allowed (preflight answer)
Access-Control-Allow-Headersrequest headers allowed, e.g. Authorization, Content-Type
Access-Control-Allow-Credentials: truecookies may be sent and read; then Allow-Origin cannot be *, it must name the origin
Access-Control-Max-Ageseconds the browser may cache the preflight

On the client, cookies cross origins only with fetch(url, { credentials: 'include' }).

Idea 7: Cookies, briefly

A cookie is a small string the server sets with Set-Cookie and the browser sends back on later requests to that site.

resp = make_response({"ok": True})
resp.set_cookie("session_id", "abc123",
                httponly=True,        # JavaScript cannot read it (document.cookie)
                secure=True,          # sent only over HTTPS
                samesite="Strict",    # not sent on cross-site requests
                max_age=3600)         # seconds; omit for a session cookie
AttributeEffect
HttpOnlyhides it from JavaScript; blocks theft through XSS
Secureonly over HTTPS
SameSite=Strictnever on cross-site requests; Lax sends on top-level navigation by GET; None always (and needs Secure)
Domain, Pathwhich hosts and URL paths receive it
Max-Age / Expireslifetime; without it, a session cookie that is deleted when the browser closes

Real answers: the pair that makes a cookie HTTPS-only and blocks cross-site requests is secure=True, samesite='Strict'. A cookie is the right choice for data that "must be sent to the server with every request"; a session cookie is sent with each request by default. A session cookie vanishes when the browser closes; sessionStorage survives a refresh but dies with the tab.

Idea 8: Designing good APIs

PracticeGoodBad
Nouns, plural, lower case/api/v1/products/getAllProducts
Hierarchy/api/v1/users/7/orders/api/v1/getOrdersOfUser?id=7
Verb is the HTTP methodDELETE /products/5POST /deleteProduct
Right status codes201 after create, 204 after delete, 404 missing, 400/422 bad input200 for everything
One error shape{"error": "name is required", "field": "name"}sometimes text, sometimes HTML
PaginationGET /products?page=2&per_page=20return 50,000 rows
Filtering and sorting?category=toys&sort=priceseparate endpoints per filter
Versioning/api/v1/... so old clients keep workingbreaking changes in place
@app.get("/api/v1/products")
def list_products():
    page = request.args.get("page", 1, type=int)
    per_page = min(request.args.get("per_page", 20, type=int), 100)
    q = Product.query.order_by(Product.id)
    p = db.paginate(q, page=page, per_page=per_page, error_out=False)
    return jsonify(items=[x.to_dict() for x in p.items], page=p.page,
                   pages=p.pages, total=p.total)
  • Idempotency: GET, PUT and DELETE repeated give the same final state. POST is not idempotent (two POSTs may create two rows). For safe retries of POST, clients may send an idempotency key.
  • Rate limiting: limit calls per user or IP (Flask-Limiter); respond 429 Too Many Requests.
  • HATEOAS: the response contains links to what you can do next ("links": {"self": ..., "next": ...}) so a client follows links instead of building URLs. It is an idea to know; few APIs do it fully.
  • Permalink: a stable URL for one resource that does not change, not a session-bound or temporary URL.
  • REST versus GraphQL: REST has many endpoints and may over-fetch or need several calls ("chatty"); GraphQL has one endpoint and the client asks for exactly the fields, usually returning 200 even for errors, with an errors field; writes are explicit mutations.
  • OpenAPI recap: a YAML description of your endpoints, parameters and responses, used for documentation and testing. Write it once, tools generate Swagger pages.
  • Sending email from an API: do not send inside the request. Use smtplib, Flask-Mail or a mail service, and move the sending to a background job (week 9). Never put the SMTP password in code; read it from the environment.
Solved example: 2. A login in Vue that survives refresh and sends the header
// store action
async login({ commit }, creds) {
  const res = await fetch('http://localhost:5000/api/v1/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(creds)
  })
  if (!res.ok) throw new Error('bad credentials')
  const { access_token } = await res.json()
  commit('setToken', access_token)            // mutation also writes sessionStorage
}

// any API call
fetch('http://localhost:5000/api/v1/me', {
  headers: { Authorization: 'Bearer ' + this.$store.state.token }
})

Because the requests are to another origin, carry JSON and an Authorization header, the browser sends a preflight OPTIONS first: the Flask side must allow the origin and the Authorization header, or you will see a CORS error in the console even though the API is fine.

Watch out - real exam trap
  • 401 means "not authenticated", 403 means "authenticated but forbidden". They are not the same.
  • Base64 is not encryption: a JWT payload is readable.
  • A JWT is signed, not secret: do not store passwords in it.
  • Same origin means same port too.
  • Wildcard Access-Control-Allow-Origin: * cannot be combined with credentials.
  • Hiding a Vue route is not security; the server must check.
  • A token must be sent on every request; the server remembers nothing.
In your project

Your project likely has roles (admin, sponsor, influencer, doctor, and so on). Examiner questions: "How do you block a normal user from an admin API?" (show the decorator that returns 403), "What is in your token and how long does it live?", "Where is the token stored and why?", "Why do you get CORS errors, and how did you fix them?" (Flask-CORS with your Vue origin). Know your status codes and keep one error JSON shape.

Your turn: 1. Decode the claim

A JWT payload decodes to {"sub":"7","role":"user","exp":1700000000}. A user changes role to admin in the payload and sends the token back. What does the server do and why?

Need a hint?

What is the signature computed over?

Show the full solution

It rejects the token with an invalid-signature error, giving 401. The signature covers header and payload with the server's secret; changing the payload without the secret leaves the old signature, which no longer matches.

Your turn: 2. Preflight or not?

A page at http://localhost:8080 runs fetch('http://localhost:5000/api/v1/me', {headers: {Authorization: 'Bearer x'}}). Is it same-origin? Is there a preflight? Which header must the server allow?

Need a hint?

Compare ports; check for custom headers.

Show the full solution

Different origin (port 8080 versus 5000). Yes, a preflight OPTIONS, because Authorization is a non-simple header. The server must send Access-Control-Allow-Origin: http://localhost:8080 and Access-Control-Allow-Headers including Authorization.

Your turn: 3. Fix the API names

Improve: POST /createNewUser, GET /getUserOrders?userId=3, POST /removeOrder?id=9. Give the status code for a successful delete.

Need a hint?

Nouns, plural, HTTP verb carries the action.

Show the full solution

POST /api/v1/users (201), GET /api/v1/users/3/orders (200), DELETE /api/v1/orders/9 (204 No Content, or 200).

Your turn: 4. Choose the cookie flags

You set a login cookie. It must not be readable by JavaScript, must go only over HTTPS and must not be sent from other sites. Write the call.

Need a hint?

Three keyword arguments.

Show the full solution

resp.set_cookie("session_id", value, httponly=True, secure=True, samesite="Strict").

Exam radar

Exam radar

In the graded papers, keyword matching finds about 77 distinct questions touching this week's themes (token auth, JWT, CORS, cookies, CSRF, web storage, REST versus GraphQL, and ephemeral state), with about 76 of 608 Quiz 2 and 146 of 1471 End Term questions matching; the count is approximate and overlaps neighbouring weeks, and the token auth, JWT and CORS content shows up as a handful of questions each year rather than a whole paper. What they ask:

  • JWT facts: three parts, base64, joined by periods; contains encoded JSON, not XML; used as an access token; the Flask-JWT-Extended get_jwt_identity(); why JWT suits many servers.
  • Token auth true/false: tokens expire (and the time can be customised); the token goes with every request; Flask-Security does not protect all endpoints by default.
  • CORS: servers declare allowed origins; not needed for same origin; primary purpose "control which domains can access resources"; Flask does not send CORS headers by default.
  • Cookies and CSRF: secure=True, samesite='Strict'; a cookie is the way to send data back automatically; the delete?id=1 image scenario is CSRF; the purpose of a CSRF token.
  • Browser storage: sessionStorage per tab and survives refresh, localStorage persists; predict what a Vue component shows after refresh when it reads and writes storage; REST versus GraphQL statements.
Watch out - real exam trap
  • "Flask enforces CSRF by default" and "Flask returns CORS headers by default" are both false.
  • sessionStorage is not shared across tabs and is not lost on refresh.
  • Local storage data is not synced across devices.
  • Selecting secure=False in the cookie question, or samesite='None' for blocking cross-site, is wrong.

Quick quiz

Quick quiz

Which are true about a JWT? (select all that apply)

  • It has three components: header, payload and signature.
  • Each component is base64-encoded.
  • The components are joined with periods.
  • It contains XML-encoded data.
A JWT is header.payload.signature, each part base64url-encoded JSON (signature raw bytes encoded).

Which statement about token-based authentication is FALSE?

  • The token usually expires after a configurable time.
  • The client sends the token only with the first request.
  • A cross-origin fetch to Flask fails by default because of CORS.
  • Flask-Security protects a route only if you decorate it.
The token must be sent with every protected request; the server holds no memory of the client.

Which pair makes a cookie HTTPS-only and blocks cross-site sending?

  • secure=False, samesite='Strict'
  • secure=True, samesite='None'
  • secure=True, samesite='Strict'
  • secure=False, samesite='Lax'
Secure limits it to HTTPS and SameSite=Strict stops it travelling on cross-site requests.

A malicious page with an image whose address is http://example1.com/delete?id=1 deletes a logged-in user's resource. This attack is...

  • Cross-site scripting
  • Cross-site request forgery
  • Session hijacking by packet sniffing
  • SQL injection
The browser attached the user's cookie automatically to a request the user never intended.

Select the correct statements about CORS. (select all that apply)

  • It lets a server say which origins may load its resources.
  • It is not required for same-origin requests.
  • It lets the client choose which servers it will accept.
  • It encrypts traffic between browser and server.
CORS is a server-declared policy, enforced by browsers, and is about origins, not encryption.

A user with a valid token but the wrong role calls an admin-only API. The best status code is...

  • 200
  • 401
  • 403
  • 404
403 Forbidden: identified but not allowed. 401 is for missing or invalid credentials.

Viva: say it out loud

Viva: say it out loud
How does token authentication work in your project?

The user posts the credentials to the login endpoint. The server checks the hashed password and returns a token with an expiry. My Vue app stores it in the Vuex store and in browser storage, and sends it in the header on every API call. The server validates the token on each protected route and returns 401 if it is missing or expired.

What is a JWT and why is the payload readable but safe from tampering?

It is header.payload.signature, each part base64url-encoded. Base64 is just encoding, so anyone can read the claims. The signature is computed with a secret key known only to the server, so if somebody changes the payload the signature no longer matches and the token is rejected.

How do you implement roles?

The role is stored on the user and put in the token or looked up from it. A decorator on the Flask route checks the role and returns 403 if it does not match. In Vue Router I also use a beforeEach guard to hide admin pages, but that is only for user experience; the server check is the real security.

Where do you store the token, and what are the risks?

In the Vuex store, backed by localStorage or sessionStorage so a refresh does not log the user out. The risk is XSS, because script on the page can read storage. A cookie with HttpOnly hides it from script but brings CSRF risk, which needs SameSite and a CSRF token. Short expiry limits the damage.

What is CORS and why did you get an error?

The browser blocks JavaScript from one origin from reading responses of another origin unless the server allows it. My Vue app on port 8080 calls Flask on port 5000, which is a different origin, and the request has JSON and an Authorization header, so there is a preflight OPTIONS. I enabled Flask-CORS for my front-end origin.

Give three API design rules you followed.

Plural nouns in URLs with the HTTP verb as the action, such as DELETE /api/v1/orders/9. Correct status codes such as 201, 404 and 403 with one JSON error shape. Pagination and a version prefix /v1 so the API can grow without breaking clients.

What is the difference between 401 and 403?

401 means the server does not know who you are, because the token is missing, wrong or expired. 403 means it knows who you are but your role does not allow this action.

What is a refresh token?

A long-lived token used only to get a new short-lived access token. The access token expires quickly, which limits theft, while the refresh token saves the user from logging in again.

Recap

Remember this
  • Session: state on the server, id in a cookie. Token: state in the signed token, sent in a header on every request; scales across servers.
  • JWT = header.payload.signature, base64url, joined with dots. Readable, not forgeable. Short-lived access token plus refresh token.
  • Flask-Security: @auth_required('token'), header Authentication-Token, login with ?include_auth_token. Flask-JWT-Extended: create_access_token, @jwt_required(), get_jwt_identity(), header Authorization: Bearer.
  • RBAC: check the role on the server (403). Vue Router guards only improve the experience.
  • Token storage: memory, sessionStorage, localStorage have XSS risk; cookies add CSRF risk unless HttpOnly, Secure, SameSite.
  • CORS: origin = scheme + host + port; non-simple requests trigger a preflight OPTIONS; servers answer with Access-Control-Allow-*.
  • API design: plural nouns, verbs, status codes, one error JSON, pagination, versioning, idempotency, rate limiting (429).
Code and facts to keep
  • Send token: Authorization: Bearer <token> or Authentication-Token: <token>
  • 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable, 429 Too Many Requests, 500 Server Error
  • PyJWT: jwt.encode(payload, key, algorithm="HS256"), jwt.decode(token, key, algorithms=["HS256"])
  • Flask-JWT-Extended: create_access_token(identity=str(id)), @jwt_required(), get_jwt_identity(), get_jwt()
  • Flask-CORS: CORS(app, origins=["http://localhost:8080"])
  • Cookie: resp.set_cookie(name, val, httponly=True, secure=True, samesite="Strict", max_age=3600)
  • fetch with cookies cross-origin: credentials: 'include', server needs Access-Control-Allow-Credentials: true
  • Idempotent: GET, PUT, DELETE. Not idempotent: POST.
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All Quiz 2 papers