API Security, Authentication and Authorization Questions
Controlling who can call an API, what they may do, and defending it against abuse. Covers the access-control mechanics: API keys, OAuth 2.0 flows, OpenID Connect, JWT issuance/validation, session vs. token auth, scopes/roles for fine-grained authorization, token lifetime and refresh, mutual TLS, and machine-to-machine vs. user-delegated access. Also covers the adversarial hardening view: input validation, injection and deserialization risks, broken object-level authorization (BOLA), mass assignment, secrets handling, and the OWASP API Security Top 10, plus securing data in transit, preventing enumeration/scraping, and testing APIs for vulnerabilities.
Explain Cross-Origin Resource Sharing (CORS): the headers involved, the browser enforcement model, and what security guarantees CORS does and doesn't actually provide. Then walk through why a wildcard Access-Control-Allow-Origin combined with Access-Control-Allow-Credentials: true is a dangerous configuration, and how you'd safely configure CORS on an API that uses cookies or bearer tokens.
Sample Answer
Direct answer
Cross-Origin Resource Sharing (CORS) is a browser-enforced relaxation of the same-origin policy that lets a web page on one origin ask a server on another origin to opt in to being called from JavaScript. It is enforced entirely client-side by the browser, so it protects browser-based callers from a malicious page, but it does nothing to stop a non-browser caller, curl, another server, a script, since none of those ever consult CORS headers at all.
Structured elaboration
Headers involved
- Request side:
Origin, the browser telling the server where the request came from. - Response side:
Access-Control-Allow-Origin(which origin or origins may read the response),Access-Control-Allow-Credentials(whether cookies or HTTP auth may be included), andAccess-Control-Allow-Methods/Access-Control-Allow-Headers(what a preflight check actually permits). - For non-simple requests, custom headers, methods like
PUTorDELETE, certain content types, the browser first sends anOPTIONSpreflight request and only proceeds with the real request if the preflight response allows it.
Browser enforcement model
This is the most commonly misunderstood part: for many requests, the server still processes and can still respond to a cross-origin call even when the origin isn't allowed. What CORS actually blocks is the browser handing that response back to the calling page's JavaScript. CORS is a response-reading gate enforced by browsers, not a request-blocking firewall, and it provides zero protection against server-to-server calls or any tool that simply does not implement browser CORS rules.
What CORS does and doesn't guarantee
- Does: stop a malicious website from using a logged-in victim's browser and its ambient cookies to read data back cross-origin via JavaScript, when configured correctly.
- Doesn't: authenticate or authorize anyone, protect an API from direct non-browser calls, or substitute for real server-side access control.
Why the wildcard-plus-credentials combination is dangerous
Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true is explicitly disallowed by the CORS specification itself, browsers refuse to honor that exact literal combination. But a very common misconfiguration achieves the same dangerous effect without triggering that block: reflecting whatever Origin header the request sent back as the allowed origin, for every single request, while also setting Access-Control-Allow-Credentials: true. That passes spec validation (it is technically not a literal wildcard) but functionally means any website in the world can make a credentialed, cookie-carrying request to this API and read the response, which lets a malicious site silently ride a logged-in user's session and exfiltrate their data.
Safe configuration for cookie or bearer-token APIs
Maintain an explicit allowlist of known, trusted origins, your own frontend domains and named partner domains, and validate the incoming Origin header against that list, only echoing it back, never a wildcard, when it actually matches. Set Access-Control-Allow-Credentials: true only on that narrow, allowlist-matched response, never combined with a reflect-anything policy. For bearer-token APIs that do not rely on cookies at all, sending the token in an Authorization header instead, you can often skip credentialed CORS entirely, since a stolen or reflected CORS configuration cannot ambiently attach a header a malicious page does not know to send.
Worked example
Dangerous configuration: a request arrives with Origin: https://evil.example, and the server responds with Access-Control-Allow-Origin: https://evil.example plus Access-Control-Allow-Credentials: true, for every incoming origin, with no allowlist check at all.
Safe configuration: the server checks the incoming Origin against an explicit list, ["https://app.example.com", "https://partner.example.com"], echoes it back only on a match, and either returns a 4xx or simply omits the CORS headers (which the browser then treats as a block) when there is no match.
Trade-offs & pitfalls
"Just reflect the Origin header, it's easier than maintaining an allowlist" is precisely the dangerous shortcut described above. CORS misconfiguration is a browser-side control, so testing it with curl or Postman will not reveal the vulnerability the way it actually manifests in the real world; you have to test from an actual disallowed origin inside a browser, or reason directly about the response headers, since curl never enforced CORS in the first place and a working curl test creates a false sense of security.
How would you design a secure refresh-token strategy for a Single Page Application (SPA) with a backend API serving 1M users? Requirements: mitigate refresh-token theft, enable revocation, minimize user friction, support refresh-token rotation and offline access. Describe storage, rotation, revocation lists or introspection, and trade-offs between stateless and stateful approaches.
Sample Answer
Direct answer
Store the refresh token in an HttpOnly, Secure, SameSite cookie, never in localStorage, where any Cross-Site Scripting (XSS) vulnerability on the page could read and exfiltrate it. Rotate the refresh token on every use, issuing a new one and invalidating the old one atomically, and treat the reuse of an already-rotated-away token as a theft signal that revokes the whole token family, not just that one token.
Structured elaboration
Storage
The refresh token lives in an HttpOnly, Secure, SameSite=Strict (or Lax, if legitimate cross-site navigation into the app is a real requirement) cookie, scoped to the specific refresh endpoint path so it is not sent on every request, shrinking its exposure to only the one call that actually needs it. The short-lived access token can live in memory (a JavaScript variable, never persisted storage) since it is replaced constantly anyway.
Rotation
Every call to the refresh endpoint issues a brand-new refresh token and invalidates the previous one, atomically, on the server. This bounds the value of a stolen refresh token to a single use before it is dead.
Revocation and theft detection
Keep a lightweight server-side record per refresh-token "family," a chain created at login where each rotation produces a new generation of the same family. If a token that has already been rotated away is presented again, that is a strong signal an attacker copied it and is now racing the legitimate user, or that the legitimate user's old token leaked somehow. The response is to revoke the entire family immediately, forcing full reauthentication, rather than trying to guess which of the two callers is the real one.
Minimizing user friction
Refresh silently in the background, using the HttpOnly cookie, proactively before the access token expires rather than only reacting to a 401 response, so the user never sees a login prompt during a normal active session. Rotation itself stays invisible to the user; only a detected theft forces a real reauthentication.
Offline access
For a browser-based SPA, "offline" mainly means the cookie persists across tab close and reopen for a defined session lifetime, with a sliding absolute cap (re-require login after some maximum session age regardless of activity), so a stolen-but-undetected token cannot live forever. This differs from a native or mobile offline-access story, which would rely on device-bound secure storage and a longer-lived credential instead.
Stateless vs stateful trade-off
A pure stateless JWT (JSON Web Token) refresh token, with no server-side record at all, cannot support real-time revocation or reuse detection, since there is nothing to check against. This design deliberately keeps some server-side state, the token-family record, specifically to make revocation and theft detection possible. The token-family record described above is effectively a lightweight, purpose-built revocation list, one row per active family rather than one row per token. The alternative pattern is introspection, where every refresh call is validated by a live call to a central token-issuing service instead of a local record lookup; introspection centralizes the decision even further and makes revocation instantaneous everywhere at once, but adds a network round trip to every single refresh and makes the introspection service itself a shared, latency-sensitive dependency at 1M-user scale. The token-family approach above is the better fit here because refreshes are already infrequent relative to access-token use, so the extra state stays small, cheap, and does not need the always-on network dependency introspection would add.
Worked example (token-family rotation and theft detection)
sequenceDiagram
participant User as Legit browser
participant Attacker
participant API as Backend API
User->>API: Login
API-->>User: Refresh token (family F1, gen 0)
User->>API: Refresh using gen 0
API-->>User: New refresh token (gen 1)
Note over API: gen 0 marked used, invalid
Attacker->>API: Refresh using stolen gen 0
API-->>Attacker: Reject: gen 0 already used
Note over API: Reuse of a rotated-away token detected, revoke entire family F1
API-->>User: Next refresh attempt also fails, forces full re-login
Whichever side, the legitimate user or the attacker, presents the already-rotated-away token second is treated identically: the whole family is revoked, and the legitimate user is forced to re-authenticate. This is a deliberate trade-off (a real user occasionally gets logged out due to a race, see pitfalls below) in exchange for reliably catching theft without guessing.
Trade-offs & pitfalls
Rotating on every call sounds airtight but can break under legitimate concurrency, two open tabs from the same real user issuing near-simultaneous refresh calls can look identical to an attack. Handle this with a short grace window that accepts the immediately-prior token once, rather than an absolute single-use rule that locks out a legitimate second tab. Storing the refresh token anywhere JavaScript can read it, localStorage or sessionStorage, defeats this entire design regardless of how good the rotation logic is, since an XSS vulnerability bypasses all of it in one step. At 1M users, the token-family store needs to be a fast, horizontally scalable keyed cache, not a heavy relational table scanned on every refresh call.
Explain the difference between authentication and authorization in the API context. Describe two common authentication methods (JWT bearer tokens and OAuth2 Authorization Code flow) and two authorization models (role-based access control RBAC and attribute-based access control ABAC). For each, give a short example of when it is appropriate.
Sample Answer
Authentication answers "who is calling?" and happens once, at the start of a request. Authorization answers "what is this caller allowed to do?" and gets checked on every action the caller attempts, even after authentication succeeds. In an API, a request must clear both gates, in that order, before it returns data.
Two ways to authenticate
JSON Web Token (JWT) bearer tokens. The server issues a signed token containing claims (who the user is, when it expires, sometimes their role). The client sends it as Authorization: Bearer <token>, and the server verifies the signature and expiry locally, without a database lookup. This is appropriate for stateless APIs and microservices where low latency and easy horizontal scaling matter more than instant revocation, for example a mobile app calling your own backend.
OAuth 2.0 Authorization Code flow. A standardized way for one application to get delegated access to a user's resources on another service, without ever seeing the user's password. The user is redirected to the authorization server's login page, authenticates there, and the calling app receives a short-lived code it exchanges (server-side) for an access token. This is appropriate when a third-party app needs to act on a user's behalf, for example a scheduling tool asking to read someone's calendar on a different platform.
Two ways to authorize
Role-Based Access Control (RBAC). Users are assigned roles, and roles carry a fixed set of permissions. It is simple to reason about and easy to audit ("who has the admin role?"). Appropriate for systems where permissions map cleanly onto job functions, for example an internal content tool with admin, editor, and viewer roles.
Attribute-Based Access Control (ABAC). The decision is computed from attributes of the user, the resource, the action, and the environment, evaluated against a policy at request time. Appropriate when access depends on runtime context a role alone can't express, for example "an employee may view a customer record only if that customer is in their assigned region."
Worked example
Request: GET /api/invoices/482 with a bearer JWT.
- Authentication: the API verifies the JWT's signature against the issuer's public key and checks
exp(the expiry timestamp). If either check fails, the response is401 Unauthorizedand processing stops here. If both pass, the caller's identity is established, saysub: "user_9fa2",role: "billing_viewer". - Authorization under RBAC: role
billing_viewercarries the permissioninvoice:read. A naive RBAC check only asks "does this role haveinvoice:read?" and answers yes, it never asks whether invoice 482 actually belongs touser_9fa2. - Authorization under ABAC: the policy instead evaluates
action == "read" AND resource.owner_id == subject.customer_id. If invoice 482 belongs to a different customer,resource.owner_id != subject.customer_id, and the policy returns deny,403 Forbidden, even though the same user'sbilling_viewerrole would pass a plain RBAC check.
This is the concrete difference between "can this role read invoices in general" (RBAC) and "can this specific caller read this specific invoice, right now" (ABAC).
Trade-offs and pitfalls
A common mistake is treating a passed authentication check as if it were also an authorization decision, that gap is exactly how broken-authorization bugs happen. RBAC is cheap to evaluate and simple to audit, but it is coarse: it cannot express "only your own records" without an added object-level check. ABAC is expressive enough to capture exactly that, but policies are harder to test exhaustively and can hide the real rule set inside a policy engine instead of a readable role list. Many production systems use both: RBAC for coarse feature access, ABAC (or a simpler ownership check) layered on top for record-level ownership.
That is every published API Security, Authentication and Authorization question for Frontend Developer so far. Browse the other topics in this category, or practice this one interactively.